From 31aaf0d13dd1a4cf3af830b742b8058d6f7a55c8 Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Thu, 19 Feb 2026 09:33:09 +0300 Subject: [PATCH] =?UTF-8?q?ML=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Configuration/GlobalAssemblyInfo.cs | 2 +- Configuration/Version.defs | 4 +- Release/pabcversion.txt | 2 +- ReleaseGenerators/PascalABCNET_version.nsh | 2 +- .../TreeConversion/CompilationErrors.cs | 4 +- bin/Lib/DataFrameABCCore.pas | 29 +- bin/Lib/LinearAlgebraML.pas | 113 ++- bin/Lib/MLABC.pas | 6 +- bin/Lib/MLCoreABC.pas | 2 +- bin/Lib/MLModelsABC.pas | 849 +++++++++++++++--- bin/Lib/PreprocessorABC.pas | 119 ++- bin/Lng/Eng/SemanticErrors_nv.dat | 4 +- bin/Lng/Rus/SemanticErrors_nv.dat | 4 +- bin/Lng/Ukr/SemanticErrors_nv.dat | 2 +- 14 files changed, 930 insertions(+), 212 deletions(-) diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 7fe652d0f..c6b50b099 100644 --- a/Configuration/GlobalAssemblyInfo.cs +++ b/Configuration/GlobalAssemblyInfo.cs @@ -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 = "3756"; + public const string Revision = "3761"; public const string MainVersion = Major + "." + Minor; public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision; diff --git a/Configuration/Version.defs b/Configuration/Version.defs index 90799d7a5..30ae61d5c 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ -%COREVERSION%=1 -%REVISION%=3756 %MINOR%=11 +%REVISION%=3761 +%COREVERSION%=1 %MAJOR%=3 diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 2bb7a7a97..a01528214 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3756 +3.11.1.3761 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index e0d4ef132..72b6bb0b2 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3756' +!define VERSION '3.11.1.3761' diff --git a/TreeConverter/TreeConversion/CompilationErrors.cs b/TreeConverter/TreeConversion/CompilationErrors.cs index 13da573dc..69cd5a841 100644 --- a/TreeConverter/TreeConversion/CompilationErrors.cs +++ b/TreeConverter/TreeConversion/CompilationErrors.cs @@ -3185,8 +3185,8 @@ namespace PascalABCCompiler.TreeConverter if (cfn is common_method_node && (cfn as common_method_node).is_constructor) return StringResources.Get("CONSTRUCTOR_PREDEFINITION_WITHOUT_DEFINITION"); if (cfn.return_value_type == null) - return StringResources.Get("PROCEDURE_PREDEFINITION_WITHOUT_DEFINITION"); - return StringResources.Get("FUNCTION_PREDEFINITION_WITHOUT_DEFINITION"); + return string.Format(StringResources.Get("PROCEDURE_PREDEFINITION_{0}_WITHOUT_DEFINITION"),cfn.name); + return string.Format(StringResources.Get("FUNCTION_PREDEFINITION_{0}_WITHOUT_DEFINITION"), cfn.name); } } diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index 78a96a78b..e3e221889 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -259,7 +259,18 @@ const 'Индекс {0} вне диапазона [0..{1})!!Index {0} out of range [0..{1})'; ER_INDICES_NULL = 'indices не может быть nil!!indices is nil'; - + ER_LEFT_SCHEMA_NULL = + 'Left schema не может быть nil!!Left schema cannot be nil'; + ER_RIGHT_SCHEMA_NULL = + 'Right schema не может быть nil!!Right schema cannot be nil'; + ER_JOIN_KEYS_LENGTH_MISMATCH = + 'Длины leftKeys и rightKeys не совпадают!!join keys length mismatch'; + ER_UNKNOWN_COLUMN_TYPE = + 'Неизвестный тип столбца!!Unknown column type'; + ER_ROW_INDEX_OUT_OF_RANGE = + 'Индекс строки {0} вне диапазона [0..{1})!!' + + 'Row index {0} out of range [0..{1})'; + //----------------------------- // Сервисные функции //----------------------------- @@ -370,7 +381,7 @@ begin begin var k := indices[i]; if (k < 0) or (k >= ColumnCount) then - raise new System.ArgumentOutOfRangeException('indices'); + ArgumentOutOfRangeError(ER_INDEX_OUT_OF_RANGE, k, ColumnCount); names[i] := fNames[k]; types[i] := fTypes[k]; if cats <> nil then cats[i] := fIsCategorical[k]; @@ -416,7 +427,7 @@ end; function DataFrameSchema.WithCategorical(name: string; value: boolean): DataFrameSchema; begin if not HasColumn(name) then - raise new System.ArgumentException($'Column "{name}" does not exist'); + ArgumentError(ER_COLUMN_NOT_EXISTS, name); var cats := if fIsCategorical = nil then new boolean[ColumnCount] else Copy(fIsCategorical); cats[IndexOf(name)] := value; @@ -428,17 +439,17 @@ class function DataFrameSchema.Merge(left, right: DataFrameSchema; leftKeys, rightKeys: array of integer; rightPrefix: string): DataFrameSchema; begin if left = nil then - raise new System.ArgumentException('left is nil'); + ArgumentNullError(ER_LEFT_SCHEMA_NULL); if right = nil then - raise new System.ArgumentException('right is nil'); + ArgumentNullError(ER_RIGHT_SCHEMA_NULL); if leftKeys.Length <> rightKeys.Length then - raise new System.ArgumentException('join keys length mismatch'); + ArgumentError(ER_JOIN_KEYS_LENGTH_MISMATCH); var skip := new boolean[right.ColumnCount]; foreach var i in rightKeys do begin if (i < 0) or (i >= right.ColumnCount) then - raise new System.ArgumentOutOfRangeException('rightKeys'); + ArgumentOutOfRangeError(ER_INDEX_OUT_OF_RANGE, i, right.ColumnCount); skip[i] := true; end; @@ -770,7 +781,7 @@ begin boolAcc[i] := pos -> c.Data[pos]; end; - else raise new Exception('Unknown column type'); + else Error(ER_UNKNOWN_COLUMN_TYPE); end; end; end; @@ -826,7 +837,7 @@ end; procedure DataFrameCursor.MoveTo(p: integer); begin if (p < 0) or (p >= rowCnt) then - raise new Exception('Cursor.MoveTo: index out of range'); + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, p, rowCnt); pos := p; end; diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index d8f90a14c..c939a20b2 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -240,7 +240,54 @@ function SolveRidge(A: Matrix; b: Vector; lambda: real := 0): Vector; implementation -uses System; +uses MLExceptions; + +const + ER_VECTOR_LENGTH_NEGATIVE = + 'Длина вектора должна быть неотрицательной!!Vector length must be non-negative'; + ER_VALUES_NULL = + 'values не может быть nil!!values cannot be nil'; + ER_VECTOR_LENGTH_MISMATCH = + 'Несоответствие длины векторов: {0} и {1}!!Vector length mismatch: {0} vs {1}'; + ER_VECTOR_EMPTY = + 'Вектор пуст!!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 = + 'Несоответствие размеров матриц: {0}x{1} и {2}x{3}!!' + + 'Matrix size mismatch: {0}x{1} vs {2}x{3}'; + ER_MATRIX_MUL_SIZE_MISMATCH = + 'Несоответствие размеров при умножении матриц: {0}x{1} * {2}x{3}!!' + + 'Matrix multiply size mismatch: {0}x{1} * {2}x{3}'; + ER_MATRIX_VECTOR_SIZE_MISMATCH = + 'Несоответствие размеров при умножении матрицы на вектор: {0}x{1} * {2}!!' + + 'Matrix-vector size mismatch: {0}x{1} * {2}'; + ER_ROW_INDEX_OUT_OF_RANGE = + 'Индекс строки {0} вне диапазона [0..{1})!!Row index {0} out of range [0..{1})'; + ER_COL_INDEX_OUT_OF_RANGE = + 'Индекс столбца {0} вне диапазона [0..{1})!!Column index {0} out of range [0..{1})'; + ER_MATRIX_NOT_SQUARE = + 'Матрица должна быть квадратной!!Matrix must be square'; + ER_MATRIX_NOT_SYMMETRIC = + 'Матрица должна быть симметричной!!Matrix must be symmetric'; + ER_PCA_K_INVALID = + 'k должно быть >= 1!!k must be >= 1'; + ER_PCA_K_TOO_LARGE = + 'k не может превышать число признаков!!k cannot exceed number of features'; + ER_PCA_NEED_TWO_SAMPLES = + 'Требуется как минимум два объекта!!At least two samples required'; + ER_CHOLESKY_NOT_SQUARE = + 'Матрица должна быть квадратной для Cholesky!!Matrix must be square for Cholesky'; + ER_MATRIX_NOT_SPD = + 'Матрица не является положительно определённой (SPD)!!Matrix is not SPD'; + ER_MATRIX_SINGULAR = + 'Матрица вырождена!!Matrix is singular'; + ER_VECTOR_SIZE_MISMATCH = + 'Несоответствие длины вектора: {0} и {1}!!Vector size mismatch: {0} and {1}'; //----------------------------- // Vector @@ -249,21 +296,21 @@ uses System; constructor Vector.Create(n: integer); begin if n < 0 then - raise new ArgumentOutOfRangeException('n', 'Vector length must be non-negative'); + ArgumentOutOfRangeError(ER_VECTOR_LENGTH_NEGATIVE); data := new real[n]; end; constructor Vector.Create(values: array of real); begin if values = nil then - raise new ArgumentNullException('values'); + ArgumentNullError(ER_VALUES_NULL); data := Copy(values); end; constructor Vector.Create(values: array of integer); begin if values = nil then - raise new ArgumentNullException('values'); + ArgumentNullError(ER_VALUES_NULL); data := values.Select(x -> real(x)).ToArray; end; @@ -275,14 +322,13 @@ end; static procedure Vector.CheckSameLength(a, b: Vector); begin if a.Length <> b.Length then - raise new ArgumentException( - $'Vector length mismatch: {a.Length} vs {b.Length}'); + ArgumentError(ER_VECTOR_LENGTH_MISMATCH, a.Length, b.Length); end; static procedure Vector.CheckNonEmpty(v: Vector); begin if v.Length = 0 then - raise new ArgumentException('Vector is empty'); + ArgumentError(ER_VECTOR_EMPTY); end; static function Vector.operator +(a, b: Vector): Vector; @@ -316,7 +362,7 @@ end; static function Vector.operator /(v: Vector; alpha: real): Vector; begin if alpha = 0.0 then - raise new DivideByZeroException('Division by zero in Vector / scalar'); + 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 @@ -388,7 +434,7 @@ function Vector.Min: real := data.Min; function Vector.Dot(b: Vector): real; begin if Length <> b.Length then - raise new Exception('Dimension mismatch in Dot'); + DimensionError(ER_DIM_MISMATCH, Length, b.Length); var s := 0.0; for var i := 0 to Length - 1 do @@ -410,14 +456,14 @@ end; constructor Matrix.Create(r, c: integer); begin if (r < 0) or (c < 0) then - raise new ArgumentOutOfRangeException('Matrix size must be non-negative'); + ArgumentOutOfRangeError(ER_MATRIX_SIZE_NEGATIVE); data := new real[r, c]; end; constructor Matrix.Create(values: array[,] of real); begin if values = nil then - raise new ArgumentNullException('values'); + ArgumentNullError(ER_VALUES_NULL); data := Copy(values); end; @@ -746,22 +792,19 @@ end; static procedure Matrix.CheckSameSize(A, B: Matrix); begin if (A.Rows <> B.Rows) or (A.Cols <> B.Cols) then - raise new ArgumentException( - $'Matrix size mismatch: {A.Rows}x{A.Cols} vs {B.Rows}x{B.Cols}'); + DimensionError(ER_MATRIX_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols); end; static procedure Matrix.CheckMulSize(A, B: Matrix); begin if A.Cols <> B.Rows then - raise new ArgumentException( - $'Matrix multiply size mismatch: {A.Rows}x{A.Cols} * {B.Rows}x{B.Cols}'); + DimensionError(ER_MATRIX_MUL_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols); end; static procedure Matrix.CheckVecSize(A: Matrix; x: Vector); begin if A.Cols <> x.Length then - raise new ArgumentException( - $'Matrix-vector size mismatch: {A.Rows}x{A.Cols} * {x.Length}'); + DimensionError(ER_MATRIX_VECTOR_SIZE_MISMATCH, A.Rows, A.Cols, x.Length); end; @@ -873,7 +916,7 @@ end; function Matrix.GetRow(i: integer): Vector; begin if (i < 0) or (i >= Rows) then - raise new ArgumentOutOfRangeException('i'); + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, Rows); Result := new Vector(Cols); for var j := 0 to Cols - 1 do Result[j] := data[i, j]; @@ -882,7 +925,7 @@ end; function Matrix.GetCol(j: integer): Vector; begin if (j < 0) or (j >= Cols) then - raise new ArgumentOutOfRangeException('j'); + ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, Cols); Result := new Vector(Rows); for var i := 0 to Rows - 1 do Result[i] := data[i, j]; @@ -891,7 +934,7 @@ end; static function Matrix.Identity(n: integer): Matrix; begin if n < 0 then - raise new ArgumentOutOfRangeException('n', 'Matrix size must be non-negative'); + ArgumentOutOfRangeError(ER_MATRIX_SIZE_NEGATIVE); Result := new Matrix(n, n); for var i := 0 to n - 1 do @@ -931,10 +974,10 @@ end; function Matrix.EigenSymmetric(tol: real; maxIter: integer): (Vector, Matrix); begin if Rows <> Cols then - raise new ArgumentException('Matrix must be square'); + ArgumentError(ER_MATRIX_NOT_SQUARE); if not IsSymmetric(tol) then - raise new ArgumentException('Matrix must be symmetric'); + ArgumentError(ER_MATRIX_NOT_SYMMETRIC); var n := Rows; @@ -1075,13 +1118,13 @@ begin var n := Cols; if k < 1 then - raise new ArgumentException('k must be >= 1'); + ArgumentError(ER_PCA_K_INVALID); if k > n then - raise new ArgumentException('k cannot exceed number of features'); + ArgumentError(ER_PCA_K_TOO_LARGE); if m < 2 then - raise new ArgumentException('At least two samples required'); + ArgumentError(ER_PCA_NEED_TWO_SAMPLES); // --- Центрирование var Xc := Clone; @@ -1136,7 +1179,7 @@ end; function Cholesky(A: Matrix): Matrix; begin if A.Rows <> A.Cols then - raise new ArgumentException('Matrix must be square for Cholesky'); + ArgumentError(ER_CHOLESKY_NOT_SQUARE); var n := A.Rows; var L := new Matrix(n, n); @@ -1151,7 +1194,7 @@ begin if i = j then begin if s <= 0.0 then - raise new InvalidOperationException('Matrix is not SPD'); + Error(ER_MATRIX_NOT_SPD); L[i, i] := Sqrt(s); end else @@ -1165,7 +1208,7 @@ end; function LUDecompose(A: Matrix): (Matrix, array of integer); begin if A.Rows <> A.Cols then - raise new ArgumentException('Matrix must be square for LU'); + ArgumentError(ER_MATRIX_NOT_SQUARE); var n := A.Rows; var LU := A.Clone; @@ -1187,7 +1230,7 @@ begin end; if maxVal = 0.0 then - raise new InvalidOperationException('Matrix is singular'); + Error(ER_MATRIX_SINGULAR); if maxRow <> k then begin @@ -1280,10 +1323,10 @@ end; function Solve(A: Matrix; b: Vector): Vector; begin if A.Rows <> A.Cols then - raise new ArgumentException('Matrix A must be square'); + ArgumentError(ER_MATRIX_NOT_SQUARE); if b.Length <> A.Rows then - raise new ArgumentException('Vector size mismatch in Solve'); + DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows); var (LU, p) := LUDecompose(A); var pb := Permute(b, p); @@ -1294,10 +1337,10 @@ end; function SolveSPD(A: Matrix; b: Vector): Vector; begin if A.Rows <> A.Cols then - raise new ArgumentException('Matrix A must be square'); + ArgumentError(ER_MATRIX_NOT_SQUARE); if b.Length <> A.Rows then - raise new ArgumentException('Vector size mismatch in SolveSPD'); + DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows); var L := Cholesky(A); var y := SolveLowerTriangular(L, b); @@ -1316,13 +1359,13 @@ end; function SolveRidge(A: Matrix; b: Vector; lambda: real): Vector; begin if lambda < 0.0 then - raise new ArgumentException('lambda must be >= 0'); + ArgumentError(ER_LAMBDA_NEGATIVE); var m := A.Rows; var n := A.Cols; if b.Length <> m then - raise new ArgumentException('Vector length mismatch in SolveRidge'); + DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, m); // ------------------------------------------------------------ // 1. Compute AtA = A^T * A diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index 579f5fa7f..a5264eda5 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -41,10 +41,10 @@ type LogisticRegression = MLModelsABC.LogisticRegression; RidgeRegression = MLModelsABC.RidgeRegression; ElasticNet = MLModelsABC.ElasticNet; - MulticlassLogisticRegression = MLModelsABC.MulticlassLogisticRegression; - + DecisionTreeClassifier = MLModelsABC.DecisionTreeClassifier; + DecisionTreeRegressor = MLModelsABC.DecisionTreeRegressor; + MLException = MLExceptions.MLException; - MLArgumentException = MLExceptions.MLArgumentException; MLNotFittedException = MLExceptions.MLNotFittedException; MLDimensionException = MLExceptions.MLDimensionException; diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 33be647aa..a121b9c73 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -32,7 +32,7 @@ type /// вместо только итогового решения. IProbabilisticClassifier = interface(IClassifier) /// Возвращает вероятность принадлежности к положительному классу для каждого объекта. - function PredictProba(X: Matrix): Vector; + function PredictProba(X: Matrix): Matrix; end; /// Интерфейс регрессионной модели. diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 72d6064a1..72c3e0ba4 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -88,64 +88,6 @@ type function Clone: IModel; end; - /// Логистическая регрессионная модель для бинарной классификации. - /// Предсказывает вероятность принадлежности объекта к классу 1 - /// на основе линейной комбинации признаков и сигмоидной функции. - /// Поддерживает L2-регуляризацию. - /// Используется в задачах бинарной классификации, - /// когда требуется вероятностный вывод и интерпретируемые коэффициенты. - LogisticRegression = class(IClassifier) - private - fCoef: Vector; - fIntercept: real; - fLambda: real; - fLearningRate: real; - fEpochs: integer; - fFitted: boolean; - public - /// Создаёт модель логистической регрессии. - /// lambda — коэффициент L2-регуляризации (0 — без регуляризации). - /// lr — шаг градиентного спуска. - /// epochs — число итераций обучения. - constructor Create(lambda: real := 0.0; lr: real := 0.1; epochs: integer := 1000); - - /// Обучает модель на числовых данных. - /// X — матрица m × n (m объектов, n признаков). - /// y — вектор длины m, содержащий метки классов (0 или 1). - function Fit(X: Matrix; y: Vector): IModel; - - /// Возвращает вероятность принадлежности к положительному классу (1) - /// для каждого объекта. - /// Результат — вектор длины m со значениями в диапазоне (0, 1). - function PredictProba(X: Matrix): Vector; - - /// Выполняет бинарную классификацию с порогом 0.5. - /// Возвращает вектор из 0 и 1. - function Predict(X: Matrix): Vector; - - /// Выполняет бинарную классификацию с заданным порогом. - /// threshold — значение в диапазоне (0, 1). - /// Если вероятность ≥ threshold, возвращается 1, иначе 0. - function Predict(X: Matrix; threshold: real): Vector; - - /// Вектор коэффициентов модели (веса признаков). - /// Длина равна числу признаков. - /// Доступен после обучения (Fit). - property Coefficients: Vector read fCoef; - - /// Свободный член модели (смещение, bias). - /// Добавляется к линейной комбинации признаков. - property Intercept: real read fIntercept; - - /// Показывает, была ли модель обучена. - /// После вызова Fit значение становится true. - property IsFitted: boolean read fFitted; - - function ToString: string; override; - - function Clone: IModel; - end; - /// Линейная регрессионная модель с L2-регуляризацией (Ridge). /// Минимизирует функцию: /// ||y - (Xβ + b)||² + λ ||β||². @@ -251,12 +193,13 @@ type function Clone: IModel; end; -/// Многоклассовая логистическая регрессия (Softmax). -/// Предсказывает вероятности принадлежности к каждому классу -/// на основе линейной комбинации признаков и softmax-функции. -/// Использует кросс-энтропийную функцию потерь. -/// Поддерживает L2-регуляризацию. - MulticlassLogisticRegression = class(IProbabilisticClassifier) +/// Логистическая регрессия. +/// Поддерживает бинарную и многоклассовую классификацию. +/// Для нескольких классов используется softmax, +/// для двух классов — частный случай softmax. +/// Оптимизация выполняется по кросс-энтропийной функции потерь +/// с поддержкой L2-регуляризации. + LogisticRegression = class(IProbabilisticClassifier) private fW: Matrix; // p x k fIntercept: Vector; // k @@ -265,6 +208,8 @@ type fEpochs: integer; fFitted: boolean; fClassCount: integer; + fClassToIndex: Dictionary; + fIndexToClass: array of integer; public /// lambda — коэффициент L2-регуляризации. /// lr — шаг градиентного спуска. @@ -273,10 +218,8 @@ type function Fit(X: Matrix; y: Vector): IModel; - function PredictProba(X: Matrix): Vector; - /// Возвращает матрицу вероятностей (m x k). - function PredictProbaMatrix(X: Matrix): Matrix; + function PredictProba(X: Matrix): Matrix; /// Возвращает вектор предсказанных классов. function Predict(X: Matrix): Vector; @@ -287,6 +230,101 @@ type function Clone: IModel; end; + + DecisionTreeNode = class + public + IsLeaf: boolean; + FeatureIndex: integer; + Threshold: real; + Left: DecisionTreeNode; + Right: DecisionTreeNode; + ClassValue: integer; + Value: real; + + function Clone: DecisionTreeNode; + end; + + SplitResult = record + Found: boolean; + Feature: integer; + Threshold: real; + end; + + DecisionTreeBase = abstract class + protected + fRoot: DecisionTreeNode; + fMaxDepth: integer; + fMinSamplesSplit: integer; + fMinSamplesLeaf: integer; + fFitted: boolean; + + function BuildTree( + X: Matrix; y: Vector; + indices: array of integer; + depth: integer + ): DecisionTreeNode; + + function FindBestSplit( + X: Matrix; y: Vector; + indices: array of integer + ): SplitResult; + + function LeafValue( + y: Vector; indices: array of integer + ): real; virtual; abstract; + + function LeafNode(value: real): DecisionTreeNode; + + function NodeImpurity(y: Vector; indices: array of integer): real; virtual; abstract; + + procedure CopyBaseState(dest: DecisionTreeBase); + public + constructor Create( + maxDepth: integer := 10; + minSamplesSplit: integer := 2; + minSamplesLeaf: integer := 1 + ); + + function IsFitted: boolean; + end; + + DecisionTreeClassifier = class(DecisionTreeBase, IClassifier) + private + fClassToIndex: Dictionary; + fIndexToClass: array of integer; + fClassCount: integer; + + function PredictOne(x: Vector): integer; + function MajorityClass(y: Vector; indices: array of integer): integer; + function Gini(y: Vector; indices: array of integer): real; + + public + constructor Create(maxDepth: integer := 10; + minSamplesSplit: integer := 2; + minSamplesLeaf: integer := 1); + + function Fit(X: Matrix; y: Vector): IModel; + function Predict(X: Matrix): Vector; + function Clone: IModel; + + protected + function LeafValue(y: Vector; indices: array of integer): real; override; + function NodeImpurity(y: Vector; indices: array of integer): real; override; + end; + + DecisionTreeRegressor = class(DecisionTreeBase, IRegressor) + protected + function LeafValue(y: Vector; indices: array of integer): real; override; + function NodeImpurity(y: Vector; indices: array of integer): real; override; + + private + function PredictOne(x: Vector): real; + + public + function Fit(X: Matrix; y: Vector): IModel; + function Predict(X: Matrix): Vector; + function Clone: IModel; + end; {$endregion Models} @@ -340,8 +378,9 @@ type /// Делает предсказание function Predict(X: Matrix): Vector; - /// Возвращает вероятности (если модель поддерживает) - function PredictProba(X: Matrix): Vector; + /// Возвращает матрицу вероятностей (m x k) + /// для вероятностной модели в конце пайплайна. + function PredictProba(X: Matrix): Matrix; /// Показывает, был ли пайплайн обучен (вызван метод Fit). property IsFitted: boolean read fFitted; @@ -595,6 +634,7 @@ implementation uses MLExceptions; +{$region ErrConstants} const ER_PIPELINE_NO_STEPS = 'Pipeline должен содержать хотя бы один шаг!!Pipeline requires at least one step'; @@ -620,6 +660,7 @@ const 'Неизвестный тип FeatureScore!!Unknown FeatureScore type'; ER_SELECTKBEST_FIT_INVALID = 'Для SelectKBest необходимо вызывать Fit(X, y)!!SelectKBest requires Fit(X, y)'; +{$endregion ErrConstants} //----------------------------- // LinearRegression @@ -680,9 +721,17 @@ end; function LinearRegression.Clone: IModel; begin - Result := new LinearRegression(); -end; + var m := new LinearRegression; + m.fFitted := fFitted; + + if fCoef <> nil then + m.fCoef := fCoef.Clone; + + m.fIntercept := fIntercept; + + Result := m; +end; //----------------------------- // Activations @@ -708,7 +757,7 @@ end; // LogisticRegression //----------------------------- -constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); +(*constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); begin fLambda := lambda; fLearningRate := lr; @@ -783,7 +832,7 @@ end; function LogisticRegression.Clone: IModel; begin Result := new LogisticRegression(fLambda, fLearningRate, fEpochs); -end; +end;*) //----------------------------- @@ -848,7 +897,16 @@ end; function RidgeRegression.Clone: IModel; begin - Result := new RidgeRegression(fLambda); + var m := new RidgeRegression(fLambda); + + m.fFitted := fFitted; + + if fCoef <> nil then + m.fCoef := fCoef.Clone; + + m.fIntercept := fIntercept; + + Result := m; end; @@ -965,14 +1023,23 @@ end; function ElasticNet.Clone: IModel; begin - Result := new ElasticNet(fLambda1, fLambda2, fMaxIter, fTol); + var m := new ElasticNet(fLambda1, fLambda2, fMaxIter, fTol); + + m.fFitted := fFitted; + + if fCoef <> nil then + m.fCoef := fCoef.Clone; + + m.fIntercept := fIntercept; + + Result := m; end; //----------------------------- // MulticlassLogisticRegression //----------------------------- -constructor MulticlassLogisticRegression.Create(lambda: real; lr: real; epochs: integer); +constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); begin fLambda := lambda; fLearningRate := lr; @@ -980,37 +1047,53 @@ begin fFitted := false; end; -function MulticlassLogisticRegression.Fit(X: Matrix; y: Vector): IModel; +function LogisticRegression.Fit(X: Matrix; y: Vector): IModel; begin + if X.RowCount <> y.Length then + DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length); + var m := X.RowCount; var p := X.ColCount; - // число классов - fClassCount := Round(y.Max) + 1; + // --- internal encoding + var unique := y.data.Select(v -> integer(v)).Distinct.ToArray; - // инициализация параметров + fClassCount := unique.Length; + + fClassToIndex := new Dictionary; + SetLength(fIndexToClass, fClassCount); + + for var i := 0 to fClassCount - 1 do + begin + fClassToIndex[unique[i]] := i; + fIndexToClass[i] := unique[i]; + end; + + var yEncoded := new Vector(m); + for var i := 0 to m - 1 do + yEncoded[i] := fClassToIndex[integer(y[i])]; + + // --- init fW := new Matrix(p, fClassCount); fIntercept := new Vector(fClassCount); - // one-hot матрица + // --- one-hot var YoneHot := new Matrix(m, fClassCount); for var i := 0 to m - 1 do - YoneHot[i, Round(y[i])] := 1.0; + YoneHot[i, integer(yEncoded[i])] := 1.0; for var epoch := 1 to fEpochs do begin - // Z = XW + b var Z := X * fW; for var i := 0 to m - 1 do for var k := 0 to fClassCount - 1 do Z[i,k] += fIntercept[k]; - // softmax + // softmax (оставляем твою версию) for var i := 0 to m - 1 do begin var maxVal := Z.RowMax(i); - var sumExp := 0.0; for var k := 0 to fClassCount - 1 do @@ -1023,21 +1106,19 @@ begin Z[i,k] /= sumExp; end; - var PP := Z; + var diff := Z - YoneHot; - // градиенты - var diff := PP - YoneHot; // m x k - var gradW := X.Transpose * diff; // p x k + var gradW := X.Transpose * diff; + gradW *= 1.0 / m; // ВАЖНО - // L2 регуляризация (только веса) if fLambda <> 0 then gradW += fLambda * fW; - // обновление fW -= fLearningRate * gradW; - // градиент по intercept var gradB := diff.ColumnSums; + gradB *= 1.0 / m; // ВАЖНО + fIntercept -= fLearningRate * gradB; end; @@ -1045,8 +1126,11 @@ begin Result := Self; end; -function MulticlassLogisticRegression.PredictProba(X: Matrix): Vector; +{function LogisticRegression.PredictProba(X: Matrix): Vector; begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + var P := PredictProbaMatrix(X); var m := P.RowCount; @@ -1057,9 +1141,9 @@ begin var k := P.RowArgMax(i); Result[i] := P[i, k]; end; -end; +end;} -function MulticlassLogisticRegression.PredictProbaMatrix(X: Matrix): Matrix; +function LogisticRegression.PredictProba(X: Matrix): Matrix; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -1091,31 +1175,541 @@ begin Result := Z; end; -function MulticlassLogisticRegression.Predict(X: Matrix): Vector; +function LogisticRegression.Predict(X: Matrix): Vector; begin - var P := PredictProbaMatrix(X); + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + var P := PredictProba(X); var m := P.RowCount; Result := new Vector(m); for var i := 0 to m - 1 do - Result[i] := P.RowArgMax(i); + begin + var internalIdx := P.RowArgMax(i); + Result[i] := fIndexToClass[internalIdx]; + end; end; -function MulticlassLogisticRegression.ToString: string; +function LogisticRegression.ToString: string; begin Result := - 'MulticlassLogisticRegression(lambda=' + fLambda + + 'LogisticRegression(lambda=' + fLambda + ', lr=' + fLearningRate + ', epochs=' + fEpochs + ')'; end; -function MulticlassLogisticRegression.Clone: IModel; +function LogisticRegression.Clone: IModel; begin - Result := new MulticlassLogisticRegression(fLambda, fLearningRate, fEpochs); + var m := new LogisticRegression( + fLambda, + fLearningRate, + fEpochs + ); + + m.fFitted := fFitted; + m.fClassCount := fClassCount; + + if fW <> nil then + m.fW := fW.Clone; + + if fIntercept <> nil then + m.fIntercept := fIntercept.Clone; + + if fClassToIndex <> nil then + begin + m.fClassToIndex := new Dictionary; + foreach var kv in fClassToIndex do + m.fClassToIndex[kv.Key] := kv.Value; + end; + + if fIndexToClass <> nil then + begin + SetLength(m.fIndexToClass, Length(fIndexToClass)); + for var i := 0 to Length(fIndexToClass) - 1 do + m.fIndexToClass[i] := fIndexToClass[i]; + end; + + Result := m; end; +function LeafClass(c: integer): DecisionTreeNode; +begin + var n := new DecisionTreeNode; + n.IsLeaf := true; + n.ClassValue := c; + Result := n; +end; + +function LeafValue(v: real): DecisionTreeNode; +begin + var n := new DecisionTreeNode; + n.IsLeaf := true; + n.Value := v; + Result := n; +end; + +function SplitNode(feature: integer; threshold: real; + leftNode, rightNode: DecisionTreeNode): DecisionTreeNode; +begin + var n := new DecisionTreeNode; + n.IsLeaf := false; + n.FeatureIndex := feature; + n.Threshold := threshold; + n.Left := leftNode; + n.Right := rightNode; + Result := n; +end; + +function DecisionTreeNode.Clone: DecisionTreeNode; +begin + var n := new DecisionTreeNode; + + n.IsLeaf := IsLeaf; + n.FeatureIndex := FeatureIndex; + n.Threshold := Threshold; + n.ClassValue := ClassValue; + n.Value := Value; + + if Left <> nil then + n.Left := Left.Clone; + + if Right <> nil then + n.Right := Right.Clone; + + Result := n; +end; + + +constructor DecisionTreeBase.Create( + maxDepth: integer; + minSamplesSplit: integer; + minSamplesLeaf: integer +); +begin + fMaxDepth := maxDepth; + fMinSamplesSplit := minSamplesSplit; + fMinSamplesLeaf := minSamplesLeaf; +end; + +procedure DecisionTreeBase.CopyBaseState(dest: DecisionTreeBase); +begin + dest.fMaxDepth := fMaxDepth; + dest.fMinSamplesSplit := fMinSamplesSplit; + dest.fMinSamplesLeaf := fMinSamplesLeaf; + dest.fFitted := fFitted; + + if fRoot <> nil then + dest.fRoot := fRoot.Clone; +end; + +function DecisionTreeBase.IsFitted: boolean; +begin + Result := fFitted; +end; + +function DecisionTreeBase.LeafNode(value: real): DecisionTreeNode; +begin + var n := new DecisionTreeNode; + n.IsLeaf := true; + n.Value := value; + Result := n; +end; + +function DecisionTreeBase.BuildTree(X: Matrix; y: Vector; + indices: array of integer; depth: integer): DecisionTreeNode; +begin + // Ограничение глубины + if depth >= fMaxDepth then + exit(LeafNode(LeafValue(y, indices))); + + // Минимальное число объектов + if indices.Length < fMinSamplesSplit then + exit(LeafNode(LeafValue(y, indices))); + + // Если узел уже чистый + if NodeImpurity(y, indices) = 0.0 then + exit(LeafNode(LeafValue(y, indices))); + + // Поиск лучшего разбиения + var split := FindBestSplit(X, y, indices); + + if not split.Found then + exit(LeafNode(LeafValue(y, indices))); + + // Разделение индексов + var left := new List; + var right := new List; + + foreach var i in indices do + if X[i, split.Feature] <= split.Threshold then + left.Add(i) + else + right.Add(i); + + // Проверка минимального размера листа + if (left.Count < fMinSamplesLeaf) or + (right.Count < fMinSamplesLeaf) then + exit(LeafNode(LeafValue(y, indices))); + + // Рекурсия + var leftNode := + BuildTree(X, y, left.ToArray, depth + 1); + + var rightNode := + BuildTree(X, y, right.ToArray, depth + 1); + + // Создание split-узла + var node := new DecisionTreeNode; + node.IsLeaf := false; + node.FeatureIndex := split.Feature; + node.Threshold := split.Threshold; + node.Left := leftNode; + node.Right := rightNode; + + Result := node; +end; + +function DecisionTreeBase.FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; +begin + var bestScore := 1e308; + var bestFeature := -1; + var bestThreshold := 0.0; + + var n := indices.Length; + + for var j := 0 to X.Cols - 1 do + begin + // --- собрать пары (value, rowIndex) + var pairs: array of (real, integer); + SetLength(pairs, n); + + for var k := 0 to n - 1 do + begin + var idx := indices[k]; + pairs[k] := (X[idx, j], idx); + end; + + // --- сортировка по значению признака + pairs.Sort(p -> p.Item1); + + for var k := 1 to n - 1 do + begin + var v1 := pairs[k-1].Item1; + var v2 := pairs[k].Item1; + + if v1 = v2 then + continue; + + var threshold := (v1 + v2) / 2.0; + + var left := new List; + var right := new List; + + for var t := 0 to n - 1 do + if pairs[t].Item1 <= threshold then + left.Add(pairs[t].Item2) + else + right.Add(pairs[t].Item2); + + if (left.Count < fMinSamplesLeaf) or + (right.Count < fMinSamplesLeaf) then + continue; + + var leftArr := left.ToArray; + var rightArr := right.ToArray; + + var leftImp := NodeImpurity(y, leftArr); + var rightImp := NodeImpurity(y, rightArr); + + var weighted := + (leftArr.Length / n) * leftImp + + (rightArr.Length / n) * rightImp; + + if weighted < bestScore then + begin + bestScore := weighted; + bestFeature := j; + bestThreshold := threshold; + + // early stop — идеально чистое разбиение + if bestScore = 0.0 then + begin + Result.Found := true; + Result.Feature := bestFeature; + Result.Threshold := bestThreshold; + exit; + end; + end; + end; + end; + + Result.Found := bestFeature <> -1; + Result.Feature := bestFeature; + Result.Threshold := bestThreshold; +end; + + +function DecisionTreeClassifier.PredictOne(x: Vector): integer; +begin + var node := fRoot; + + while not node.IsLeaf do + begin + if x[node.FeatureIndex] <= node.Threshold then + node := node.Left + else + node := node.Right; + end; + + Result := integer(node.Value); // internal class index +end; + + +function DecisionTreeClassifier.MajorityClass(y: Vector; indices: array of integer): integer; +begin + var counts := new integer[fClassCount]; + + // Подсчёт частот + foreach var i in indices do + begin + var c := integer(y[i]); + counts[c] += 1; + end; + + // Поиск максимума + var bestClass := 0; + var bestCount := -1; + + for var c := 0 to fClassCount - 1 do + if counts[c] > bestCount then + begin + bestCount := counts[c]; + bestClass := c; + end; + + Result := bestClass; +end; + +function DecisionTreeClassifier.Gini(y: Vector; indices: array of integer): real; +begin + var counts := new integer[fClassCount]; + + foreach var i in indices do + counts[integer(y[i])] += 1; + + var n := indices.Length; + var sum := 0.0; + + for var c := 0 to fClassCount - 1 do + begin + var p := counts[c] / n; + sum += p * p; + end; + + Result := 1.0 - sum; +end; + +constructor DecisionTreeClassifier.Create(maxDepth: integer; + minSamplesSplit: integer; minSamplesLeaf: integer); +begin + fMaxDepth := maxDepth; + fMinSamplesSplit := minSamplesSplit; + fMinSamplesLeaf := minSamplesLeaf; +end; + +function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): IModel; +begin + if X.Rows <> y.Length then + DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + + if X.Rows = 0 then + ArgumentError(ER_EMPTY_DATASET); + + // --- 1. Найти уникальные классы + var unique := new HashSet; + + for var i := 0 to y.Length - 1 do + unique.Add(integer(y[i])); + + fClassCount := unique.Count; + + // --- 2. Создать массив index -> original class + SetLength(fIndexToClass, fClassCount); + fClassToIndex := new Dictionary; + + var idx := 0; + foreach var c in unique do + begin + fIndexToClass[idx] := c; + fClassToIndex[c] := idx; + idx += 1; + end; + + // --- 3. Создать закодированный y + var yEncoded := new Vector(y.Length); + + for var i := 0 to y.Length - 1 do + yEncoded[i] := fClassToIndex[integer(y[i])]; + + // --- 4. Создать массив индексов строк + var indices := new integer[X.Rows]; + for var i := 0 to X.Rows - 1 do + indices[i] := i; + + // --- 5. Построить дерево + fRoot := BuildTree(X, yEncoded, indices, 0); + + fFitted := true; + + Result := Self; +end; + +function DecisionTreeClassifier.Predict(X: Matrix): Vector; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED);; + + var n := X.Rows; + Result := new Vector(n); + + for var i := 0 to n - 1 do + begin + var row := X.GetRow(i); + var internalClass := PredictOne(row); + + // вернуть оригинальную метку + Result[i] := fIndexToClass[internalClass]; + end; +end; + +function DecisionTreeClassifier.Clone: IModel; +begin + var m := new DecisionTreeClassifier( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf + ); + + CopyBaseState(m); + + m.fClassCount := fClassCount; + + if fClassToIndex <> nil then + begin + m.fClassToIndex := new Dictionary; + foreach var kv in fClassToIndex do + m.fClassToIndex[kv.Key] := kv.Value; + end; + + if fIndexToClass <> nil then + begin + SetLength(m.fIndexToClass, Length(fIndexToClass)); + for var i := 0 to Length(fIndexToClass) - 1 do + m.fIndexToClass[i] := fIndexToClass[i]; + end; + + Result := m; +end; + +function DecisionTreeClassifier.LeafValue(y: Vector; indices: array of integer): real; +begin + Result := MajorityClass(y, indices); +end; + +function DecisionTreeClassifier.NodeImpurity(y: Vector; indices: array of integer): real; +begin + Result := Gini(y, indices); +end; + +function DecisionTreeRegressor.LeafValue(y: Vector; indices: array of integer): real; +begin + var sum := 0.0; + foreach var i in indices do + sum += y[i]; + + Result := sum / indices.Length; +end; + +function DecisionTreeRegressor.NodeImpurity(y: Vector; indices: array of integer): real; +begin + var sum := 0.0; + var sqSum := 0.0; + var n := indices.Length; + + foreach var i in indices do + begin + var v := y[i]; + sum += v; + sqSum += v * v; + end; + + var mean := sum / n; + Result := (sqSum / n) - mean * mean; +end; + +function DecisionTreeRegressor.Fit(X: Matrix; y: Vector): IModel; +begin + if X.Rows <> y.Length then + DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + + if X.Rows = 0 then + ArgumentError(ER_EMPTY_DATASET); + + var indices := new integer[X.Rows]; + for var i := 0 to X.Rows - 1 do + indices[i] := i; + + fRoot := BuildTree(X, y, indices, 0); + fFitted := true; + + Result := Self; +end; + +function DecisionTreeRegressor.PredictOne(x: Vector): real; +begin + var node := fRoot; + + while not node.IsLeaf do + begin + if x[node.FeatureIndex] <= node.Threshold then + node := node.Left + else + node := node.Right; + end; + + Result := node.Value; +end; + +function DecisionTreeRegressor.Predict(X: Matrix): Vector; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + var n := X.Rows; + Result := new Vector(n); + + for var i := 0 to n - 1 do + Result[i] := PredictOne(X.GetRow(i)); +end; + +function DecisionTreeRegressor.Clone: IModel; +begin + var m := new DecisionTreeRegressor( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf + ); + + CopyBaseState(m); + + Result := m; +end; + + + //----------------------------- // Pipeline //----------------------------- @@ -1175,7 +1769,7 @@ end; function Pipeline.SetModel(m: IModel): Pipeline; begin if m = nil then - ArgumentError(ER_TRANSFORMER_NULL); + ArgumentError(ER_MODEL_NULL); fModel := m; Result := Self; @@ -1225,7 +1819,7 @@ begin Result := fModel.Predict(Xt); end; -function Pipeline.PredictProba(X: Matrix): Vector; +function Pipeline.PredictProba(X: Matrix): Matrix; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -1235,7 +1829,8 @@ begin var Xt := Transform(X); - Result := (fModel as IProbabilisticClassifier).PredictProba(Xt); + Result := (fModel as IProbabilisticClassifier) + .PredictProba(Xt); end; function Pipeline.ToString: string; @@ -1288,7 +1883,7 @@ end; function StandardScaler.Transform(X: Matrix): Matrix; begin if not fFitted then - Error(ER_PROBA_NOT_SUPPORTED); + NotFittedError(ER_FIT_NOT_CALLED); var n := X.RowCount; var p := X.ColCount; @@ -1316,10 +1911,13 @@ end; function StandardScaler.Clone: ITransformer; begin - Result := new StandardScaler(); + var s := new StandardScaler; + s.fMean := fMean.Clone; + s.fStd := fStd.Clone; + s.fFitted := fFitted; + Result := s; end; - //----------------------------- // MinMaxScaler //----------------------------- @@ -1377,10 +1975,15 @@ end; function MinMaxScaler.Clone: ITransformer; begin - Result := new MinMaxScaler(fRangeMin, fRangeMax); + var s := new MinMaxScaler; + s.fMin := fMin.Clone; + s.fMax := fMax.Clone; + s.fFitted := fFitted; + s.fRangeMin := fRangeMin; + s.fRangeMax := fRangeMax; + Result := s; end; - //----------------------------- // PCATransformer //----------------------------- @@ -1436,7 +2039,11 @@ end; function PCATransformer.Clone: ITransformer; begin - Result := new PCATransformer(fK); + var t := new PCATransformer(fK); + t.fComponents := fComponents.Clone; + t.fMean := fMean.Clone; + t.fFitted := fFitted; + Result := t; end; //----------------------------- @@ -1492,7 +2099,10 @@ end; function VarianceThreshold.Clone: ITransformer; begin - Result := new VarianceThreshold(fThreshold); + var t := new VarianceThreshold(fThreshold); + t.fSelected := Copy(fSelected); + t.fFitted := fFitted; + Result := t; end; //----------------------------- @@ -1730,13 +2340,13 @@ end; function SelectKBest.Clone: ITransformer; begin - if fScoreFunc <> nil then - Result := new SelectKBest(fK, fScoreFunc) - else - Result := new SelectKBest(fK, fScoreType); + var t := new SelectKBest(fK, fScoreType); + t.fScoreFunc := fScoreFunc; + t.fSelected := Copy(fSelected); + t.fFitted := fFitted; + Result := t; end; - //----------------------------- // Normalizer //----------------------------- @@ -1799,7 +2409,10 @@ end; function Normalizer.Clone: ITransformer; begin - Result := new Normalizer(fNormType); + var t := new Normalizer(fNormType); + t.fFitted := fFitted; + Result := t; end; + end. \ No newline at end of file diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index 0d26d6765..1d23ceb2e 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -180,6 +180,61 @@ const 'StandardScaler: столбцы не указаны!!StandardScaler: columns not specified'; ER_SCALER_COLUMN_NOT_NUMERIC = 'StandardScaler: столбец "{0}" не является числовым!!StandardScaler: column "{0}" is not numeric'; + ER_SCALER_NO_VALID_VALUES = + 'StandardScaler: столбец "{0}" не содержит допустимых значений!!' + + 'StandardScaler: column "{0}" has no valid values'; + ER_SCALER_ZERO_VARIANCE = + 'StandardScaler: нулевая дисперсия в столбце "{0}"!!' + + 'StandardScaler: zero variance in column "{0}"'; + ER_MINMAX_NO_COLUMNS = + 'MinMaxScaler: столбцы не указаны!!MinMaxScaler: columns not specified'; + ER_MINMAX_COLUMN_NOT_NUMERIC = + 'MinMaxScaler: столбец "{0}" не является числовым!!MinMaxScaler: column "{0}" is not numeric'; + ER_MINMAX_NO_VALID_VALUES = + 'MinMaxScaler: столбец "{0}" не содержит допустимых значений!!' + + 'MinMaxScaler: column "{0}" has no valid values'; + ER_MINMAX_CONSTANT_COLUMN = + 'MinMaxScaler: постоянный столбец "{0}"!!MinMaxScaler: constant column "{0}"'; + ER_LABELENCODER_NO_COLUMN = + 'LabelEncoder: столбец не указан!!LabelEncoder: column not specified'; + ER_LABELENCODER_NOT_STRING = + 'LabelEncoder: столбец "{0}" не является строковым!!' + + 'LabelEncoder: column "{0}" is not string'; + ER_LABELENCODER_NA = + 'LabelEncoder: NA значение не допускается!!LabelEncoder: NA value not allowed'; + ER_LABELENCODER_UNSEEN_CATEGORY = + 'LabelEncoder: неизвестная категория "{0}"!!' + + 'LabelEncoder: unseen category "{0}"'; + ER_ONEHOT_NO_COLUMN = + 'OneHotEncoder: столбец не указан!!OneHotEncoder: column not specified'; + ER_ONEHOT_NOT_STRING = + 'OneHotEncoder: столбец "{0}" не является строковым или не содержит допустимых значений!!' + + 'OneHotEncoder: column "{0}" is not string or has no valid values'; + ER_ONEHOT_UNSEEN_CATEGORY = + 'OneHotEncoder: неизвестная категория "{0}"!!' + + 'OneHotEncoder: unseen category "{0}"'; + ER_IMPUTER_INVALID_STRATEGY_MEAN = + 'Imputer: данный конструктор предназначен для стратегии isMean!!' + + 'Imputer: this constructor is for isMean'; + ER_IMPUTER_INVALID_STRATEGY_CONSTANT = + 'Imputer: данный конструктор предназначен для стратегии isConstant!!' + + 'Imputer: this constructor is for isConstant'; + ER_IMPUTER_NO_COLUMNS = + 'Imputer: столбцы не указаны!!Imputer: columns not specified'; + ER_IMPUTER_COLUMN_NOT_NUMERIC = + 'Imputer: столбец "{0}" не является числовым!!' + + 'Imputer: column "{0}" is not numeric'; + ER_IMPUTER_NO_VALID_VALUES = + 'Imputer(mean): столбец "{0}" не содержит допустимых значений!!' + + 'Imputer(mean): column "{0}" has no valid values'; + ER_IMPUTER_CONSTANT_VALUE_NULL = + 'Imputer(constant): значение nil для столбца "{0}"!!' + + 'Imputer(constant): value is nil for column "{0}"'; + ER_IMPUTER_CONSTANT_TYPE_MISMATCH = + 'Imputer(constant): несоответствие типа значения для столбца "{0}"!!' + + 'Imputer(constant): value type mismatch for column "{0}"'; + ER_PIPELINE_MODIFY_AFTER_FIT = + 'Нельзя добавлять шаг после вызова Fit()!!Cannot add step after Fit'; //----------------------------- // StandardScaler @@ -224,14 +279,14 @@ begin end; if cnt = 0 then - raise new Exception($'StandardScaler: column "{colName}" has no valid values'); + Error(ER_SCALER_NO_VALID_VALUES, colName); means[i] := sum / cnt; var v := sum2 / cnt - means[i] * means[i]; stds[i] := Sqrt(v); if stds[i] = 0 then - raise new Exception($'StandardScaler: zero variance in column "{colName}"'); + Error(ER_SCALER_ZERO_VARIANCE, colName); end; fitted := true; @@ -241,7 +296,7 @@ end; function DataStandardScaler.Transform(df: DataFrame): DataFrame; begin if not fitted then - raise new Exception('StandardScaler: not fitted'); + NotFittedError(ER_FIT_NOT_CALLED); var res := df; @@ -277,7 +332,7 @@ end; constructor DataMinMaxScaler.Create(params columns: array of string); begin if (columns = nil) or (columns.Length = 0) then - raise new ArgumentException('MinMaxScaler: columns not specified'); + ArgumentError(ER_MINMAX_NO_COLUMNS); cols := columns; fitted := false; @@ -296,7 +351,7 @@ begin var ct := df.Schema.ColumnTypeAt(idx); if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then - raise new Exception($'MinMaxScaler: column "{colName}" is not numeric'); + Error(ER_MINMAX_COLUMN_NOT_NUMERIC, colName); var first := true; var minv, maxv: real; @@ -321,10 +376,10 @@ begin end; if first then - raise new Exception($'MinMaxScaler: column "{colName}" has no valid values'); + Error(ER_MINMAX_NO_VALID_VALUES, colName); if minv = maxv then - raise new Exception($'MinMaxScaler: constant column "{colName}"'); + Error(ER_MINMAX_CONSTANT_COLUMN, colName); mins[i] := minv; maxs[i] := maxv; @@ -337,7 +392,7 @@ end; function DataMinMaxScaler.Transform(df: DataFrame): DataFrame; begin if not fitted then - raise new Exception('MinMaxScaler: not fitted'); + NotFittedError(ER_FIT_NOT_CALLED); var res := df; @@ -374,7 +429,7 @@ end; constructor LabelEncoder.Create(column: string); begin if column = '' then - raise new ArgumentException('LabelEncoder: column not specified'); + ArgumentError(ER_LABELENCODER_NO_COLUMN); col := column; fitted := false; @@ -385,7 +440,7 @@ begin var idx := df.Schema.IndexOf(col); if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then - raise new Exception($'LabelEncoder: column "{col}" is not string'); + Error(ER_LABELENCODER_NOT_STRING, col); mapping := new Dictionary; @@ -411,7 +466,7 @@ end; function LabelEncoder.Transform(df: DataFrame): DataFrame; begin if not fitted then - raise new Exception('LabelEncoder: not fitted'); + NotFittedError(ER_FIT_NOT_CALLED); var idx := df.Schema.IndexOf(col); @@ -419,14 +474,12 @@ begin col, c -> if not c.IsValid(idx) then - raise new Exception('NA') // будет поймано как NA + Error(ER_LABELENCODER_NA) // будет поймано как NA else begin var s := c.Str(idx); if not mapping.ContainsKey(s) then - raise new Exception( - $'LabelEncoder: unseen category "{s}"' - ); + Error(ER_LABELENCODER_UNSEEN_CATEGORY, s); Result := mapping[s]; end ); @@ -445,7 +498,7 @@ end; constructor OneHotEncoder.Create(column: string); begin if column = '' then - raise new ArgumentException('OneHotEncoder: column not specified'); + ArgumentError(ER_ONEHOT_NO_COLUMN); col := column; fitted := false; end; @@ -454,9 +507,7 @@ function OneHotEncoder.Fit(df: DataFrame): IPreprocessor; begin var idx := df.Schema.IndexOf(col); if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then - raise new Exception( - $'OneHotEncoder: column "{col}" is not string or has no valid values' - ); + Error(ER_ONEHOT_NOT_STRING, col); indexByValue := new Dictionary; var values := new List; @@ -481,7 +532,7 @@ end; function OneHotEncoder.Transform(df: DataFrame): DataFrame; begin if not fitted then - raise new Exception('OneHotEncoder: not fitted'); + NotFittedError(ER_FIT_NOT_CALLED); var srcIdx := df.Schema.IndexOf(col); var catCount := categories.Length; @@ -494,7 +545,7 @@ begin var s := cur.Str(srcIdx); if not indexByValue.ContainsKey(s) then - raise new Exception($'OneHotEncoder: unseen category "{s}"'); + Error(ER_ONEHOT_UNSEEN_CATEGORY, s); end; // === ШАГ 2. Генерация one-hot столбцов === @@ -543,10 +594,10 @@ end; constructor Imputer.Create(strategy: ImputeStrategy; params columns: array of string); begin if strategy <> isMean then - raise new ArgumentException('Imputer: this constructor is for isMean'); + ArgumentError(ER_IMPUTER_INVALID_STRATEGY_MEAN); if (columns = nil) or (columns.Length = 0) then - raise new ArgumentException('Imputer: columns not specified'); + ArgumentError(ER_IMPUTER_NO_COLUMNS); self.strategy := strategy; self.cols := columns; @@ -557,10 +608,10 @@ end; constructor Imputer.Create(strategy: ImputeStrategy; value: object; params columns: array of string); begin if strategy <> isConstant then - raise new ArgumentException('Imputer: this constructor is for isConstant'); + ArgumentError(ER_IMPUTER_INVALID_STRATEGY_CONSTANT); if (columns = nil) or (columns.Length = 0) then - raise new ArgumentException('Imputer: columns not specified'); + ArgumentError(ER_IMPUTER_NO_COLUMNS); self.strategy := strategy; self.cols := columns; @@ -586,7 +637,7 @@ begin var ct := df.Schema.ColumnTypeAt(idx); if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then - raise new Exception($'Imputer(mean): column "{name}" is not numeric'); + Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); var sum := 0.0; var cnt := 0; @@ -600,7 +651,7 @@ begin end; if cnt = 0 then - raise new Exception($'Imputer(mean): column "{name}" has no valid values'); + Error(ER_IMPUTER_NO_VALID_VALUES, name); means[i] := sum / cnt; end; @@ -613,7 +664,7 @@ end; function Imputer.Transform(df: DataFrame): DataFrame; begin if not fitted then - raise new Exception('Imputer: not fitted'); + NotFittedError(ER_FIT_NOT_CALLED); var res := df; @@ -624,7 +675,7 @@ begin var ct := df.Schema.ColumnTypeAt(idx); if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then - raise new Exception($'Imputer: column "{name}" is not numeric'); + Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); if strategy = isMean then begin @@ -638,7 +689,7 @@ begin begin var v := constants[i]; if v = nil then - raise new Exception($'Imputer(constant): value is nil for column "{name}"'); + Error(ER_IMPUTER_CONSTANT_VALUE_NULL, name); if ct = ColumnType.ctInt then begin @@ -647,7 +698,7 @@ begin k := integer(v); except on e: Exception do - raise new Exception($'Imputer(constant): value type mismatch for column "{name}"'); + Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name); end; res := res.ReplaceColumnInt( @@ -662,7 +713,7 @@ begin r := real(v); except on e: Exception do - raise new Exception($'Imputer(constant): value type mismatch for column "{name}"'); + Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name); end; res := res.ReplaceColumnFloat( @@ -695,7 +746,7 @@ end; function DataPipeline.Add(p: IPreprocessor): DataPipeline; begin if fitted then - raise new Exception('Cannot add step after Fit'); + Error(ER_PIPELINE_MODIFY_AFTER_FIT); steps.Add(p); Result := Self; @@ -718,7 +769,7 @@ end; function DataPipeline.Transform(df: DataFrame): DataFrame; begin if not fitted then - NotFittedError(ER_FIT_NOT_CALLED);NotFittedError(ER_FIT_NOT_CALLED); + NotFittedError(ER_FIT_NOT_CALLED); var current := df; foreach var step in steps do diff --git a/bin/Lng/Eng/SemanticErrors_nv.dat b/bin/Lng/Eng/SemanticErrors_nv.dat index 728be73d8..68123ad0a 100644 --- a/bin/Lng/Eng/SemanticErrors_nv.dat +++ b/bin/Lng/Eng/SemanticErrors_nv.dat @@ -70,8 +70,8 @@ FUNCTION_WITHOUT_BODY_MUST_HAVE_FORWARD_ATTRIBUTE=Function without body must hav DIFFERENT_PARAMETER_NAME_IN_FUNCTION_DEFINITION_{0}_AND_PREDEFINITION_{1}=Function predefinition and definition have different parameter name: '{0}' and '{1}' FUNCTION_DEFINITION_HAVE_DIFFERENT_PARAMS_WITH_PREDEFINITION=Function definition and predefinition have different parameters FUNCTION_PREDEFINITION_AND_DEFINITION_HAVE_DIFFERENT_RESULT_TYPES=Function predefinition and definition have different return type -FUNCTION_PREDEFINITION_WITHOUT_DEFINITION=Function has no realization -PROCEDURE_PREDEFINITION_WITHOUT_DEFINITION=Procedure has no realization +FUNCTION_PREDEFINITION_{0}_WITHOUT_DEFINITION=Function {0} has no implementation +PROCEDURE_PREDEFINITION_{0}_WITHOUT_DEFINITION=Procedure {0} has no implementation CONSTRUCTOR_PREDEFINITION_WITHOUT_DEFINITION=Constructor has no realization DIVISION_BY_ZERO_CONSTANT=Division by zero TYPE_NAME_EXPECTED=A name of type expected diff --git a/bin/Lng/Rus/SemanticErrors_nv.dat b/bin/Lng/Rus/SemanticErrors_nv.dat index f2ce0b5f4..15b147092 100644 --- a/bin/Lng/Rus/SemanticErrors_nv.dat +++ b/bin/Lng/Rus/SemanticErrors_nv.dat @@ -70,8 +70,8 @@ FUNCTION_WITHOUT_BODY_MUST_HAVE_FORWARD_ATTRIBUTE=Подпрограмма бе DIFFERENT_PARAMETER_NAME_IN_FUNCTION_DEFINITION_{0}_AND_PREDEFINITION_{1}=Различные имена параметров в описании подпрограммы {0} и ее предописании {1} FUNCTION_DEFINITION_HAVE_DIFFERENT_PARAMS_WITH_PREDEFINITION=Параметры в описании подпрограммы отличаются от параметров в ее предописании FUNCTION_PREDEFINITION_AND_DEFINITION_HAVE_DIFFERENT_RESULT_TYPES=Описание и предописание функции имеют разные типы возвращаемого значения -FUNCTION_PREDEFINITION_WITHOUT_DEFINITION=Предописание функции без описания -PROCEDURE_PREDEFINITION_WITHOUT_DEFINITION=Предописание процедуры без описания +FUNCTION_PREDEFINITION_{0}_WITHOUT_DEFINITION=Предописание функции {0} без описания +PROCEDURE_PREDEFINITION_{0}_WITHOUT_DEFINITION=Предописание процедуры {0} без описания CONSTRUCTOR_PREDEFINITION_WITHOUT_DEFINITION=Предописание конструктора без описания DIVISION_BY_ZERO_CONSTANT=Деление на константу 0 TYPE_NAME_EXPECTED=Ожидалось имя типа diff --git a/bin/Lng/Ukr/SemanticErrors_nv.dat b/bin/Lng/Ukr/SemanticErrors_nv.dat index df1cecc46..6a0c36eda 100644 --- a/bin/Lng/Ukr/SemanticErrors_nv.dat +++ b/bin/Lng/Ukr/SemanticErrors_nv.dat @@ -62,7 +62,7 @@ FUNCTION_WITHOUT_BODY_MUST_HAVE_FORWARD_ATTRIBUTE=Підпрограма без DIFFERENT_PARAMETER_NAME_IN_FUNCTION_DEFINITION_{0}_AND_PREDEFINITION_{1}=Різні імена параметрів в опису підпрограми {0} і її передопису {1} FUNCTION_DEFINITION_HAVE_DIFFERENT_PARAMS_WITH_PREDEFINITION=Параметри в опису підпрограми відрізняються від параметрів в її передопису FUNCTION_PREDEFINITION_AND_DEFINITION_HAVE_DIFFERENT_RESULT_TYPES=Опис і предопис функції мають різні типи значення повертання -FUNCTION_PREDEFINITION_WITHOUT_DEFINITION=Предопис функції без опису +FUNCTION_PREDEFINITION_{0}_WITHOUT_DEFINITION=Предопис функції {0} без опису DIVISION_BY_ZERO_CONSTANT=Ділення на константу 0 TYPE_NAME_EXPECTED=Очікувалося ім'я типу ONLY_COMMON_TYPE_METHOD_DEFINITION_ALLOWED=Неприпустимим є визначення методу класу, які не описаного в програмі