diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index 7db918b9d..6bd3d4cdd 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -29,6 +29,9 @@ type VarianceThreshold = MLModelsABC.VarianceThreshold; SelectKBest = MLModelsABC.SelectKBest; FeatureScore = MLModelsABC.FeatureScore; + Normalizer = MLModelsABC.Normalizer; + + NormType = MLModelsABC.NormType; Activations = MLModelsABC.Activations; Pipeline = MLModelsABC.Pipeline; diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 33faefa5b..33be647aa 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -5,10 +5,16 @@ interface uses LinearAlgebraML; type + /// Базовый интерфейс шага конвейера машинного обучения. + /// Используется для объединения преобразователей и моделей + /// в единую последовательность выполнения внутри Pipeline. + IPipeStep = interface end; + /// Базовый интерфейс модели машинного обучения - IModel = interface + IModel = interface(IPipeStep) function Fit(X: Matrix; y: Vector): IModel; function Predict(X: Matrix): Vector; + function Clone: IModel; end; /// Интерфейс классификатора. @@ -34,11 +40,11 @@ type /// Предназначен для моделей, предсказывающих числовые значения. IRegressor = interface(IModel) end; - + /// Базовый интерфейс преобразования признаков. /// Используется для масштабирования, отбора, /// уменьшения размерности и других преобразований данных. - ITransformer = interface + ITransformer = interface(IPipeStep) /// Обучает преобразование на данных. /// Запоминает необходимые параметры, /// которые будут использоваться при Transform. @@ -46,6 +52,7 @@ type /// Применяет обученное преобразование к данным. /// Возвращает новую матрицу признаков. function Transform(X: Matrix): Matrix; + function Clone: ITransformer; end; /// Интерфейс преобразования признаков с учётом целевой переменной. @@ -56,7 +63,7 @@ type /// как признаков X, так и целевой переменной y. /// Запоминает необходимые параметры, /// которые будут использоваться при Transform. - function Fit(X: Matrix; y: Vector): ISupervisedTransformer; + function Fit(X: Matrix; y: Vector): ITransformer; end; implementation diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 66469c287..bad6be643 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -47,18 +47,17 @@ type IModel = MLCoreABC.IModel; -/// Линейная регрессионная модель +/// Линейная регрессионная модель (метод наименьших квадратов). /// Предсказывает числовое значение по линейной комбинации признаков -/// Используется в задачах регрессии при отсутствии сильной +/// Используется в задачах регрессии при отсутствии выраженной /// мультиколлинеарности и когда число признаков существенно меньше числа объектов. LinearRegression = class(IRegressor) private fCoef: Vector; fIntercept: real; - fLambda: real; fFitted: boolean; public - constructor Create(lambda: real := 0.0); + constructor Create(); /// Обучает модель на числовых данных /// X — матрица m × n (m объектов, n признаков) @@ -83,6 +82,10 @@ type /// После вызова Fit значение становится true. /// Используется для проверки корректности вызова Predict. property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: IModel; end; /// Логистическая регрессионная модель для бинарной классификации. @@ -137,6 +140,10 @@ type /// Показывает, была ли модель обучена. /// После вызова Fit значение становится true. property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: IModel; end; /// Линейная регрессионная модель с L2-регуляризацией (Ridge). @@ -181,6 +188,10 @@ type /// Показывает, была ли модель обучена. /// После вызова Fit значение становится true. property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: IModel; end; /// Линейная регрессионная модель ElasticNet. @@ -234,7 +245,11 @@ type /// Показывает, была ли модель обучена. /// После вызова Fit значение становится true. property IsFitted: boolean read fFitted; - end; + + function ToString: string; override; + + function Clone: IModel; + end; /// Многоклассовая логистическая регрессия (Softmax). /// Предсказывает вероятности принадлежности к каждому классу @@ -267,6 +282,10 @@ type function Predict(X: Matrix): Vector; property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: IModel; end; @@ -299,12 +318,12 @@ type /// Модель должна быть установлена через SetModel. constructor Create; - /// Строит конвейер машинного обучения. - /// model — модель, обучаемая после применения всех преобразователей. - /// transformers — последовательность преобразователей признаков, - /// применяемых к данным перед обучением модели. + /// Строит конвейер машинного обучения из последовательности шагов. + /// Шаги указываются в порядке выполнения: + /// сначала преобразователи, затем модель. + /// Последний шаг обязан быть моделью (IModel). /// Возвращает сконструированный конвейер. - static function Build(model: IModel; params transformers: array of ITransformer): Pipeline; + static function Build(params steps: array of IPipeStep): Pipeline; /// Устанавливает или заменяет модель. function SetModel(m: IModel): Pipeline; @@ -326,6 +345,10 @@ type /// Показывает, был ли пайплайн обучен (вызван метод Fit). property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: IModel; end; {$endregion Pipeline} @@ -353,6 +376,10 @@ type /// Признак того, что преобразование обучено. property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: ITransformer; end; /// Масштабирует признаки в заданный диапазон @@ -387,6 +414,10 @@ type /// Признак того, что преобразование обучено. property IsFitted: boolean read fFitted; + + function ToString: string; override; + + function Clone: ITransformer; end; /// Трансформер главных компонент (PCA). @@ -417,6 +448,9 @@ type property Components: Matrix read fComponents; property Mean: Vector read fMean; property IsFitted: boolean read fFitted; + + function ToString: string; override; + function Clone: ITransformer; end; /// Трансформер, удаляющий признаки с малой дисперсией. @@ -444,6 +478,9 @@ type /// Показывает, был ли выполнен Fit. property IsFitted: boolean read fFitted; + + function ToString: string; override; + function Clone: ITransformer; end; /// Тип критерия оценки признаков для SelectKBest. @@ -481,7 +518,7 @@ type /// и оставляет k признаков с наибольшим значением score. /// Может использовать встроенные критерии /// или пользовательскую функцию оценки. - SelectKBest = class(ITransformer) + SelectKBest = class(ISupervisedTransformer) private fK: integer; fScoreType: FeatureScore; @@ -520,6 +557,9 @@ type /// Показывает, был ли выполнен Fit. property IsFitted: boolean read fFitted; + + function ToString: string; override; + function Clone: ITransformer; end; /// Тип нормы для нормализации строк. @@ -542,6 +582,9 @@ type function Transform(X: Matrix): Matrix; property IsFitted: boolean read fFitted; + + function ToString: string; override; + function Clone: ITransformer; end; @@ -556,16 +599,14 @@ uses System; // LinearRegression //----------------------------- -constructor LinearRegression.Create(lambda: real); +constructor LinearRegression.Create(); begin - self.flambda := lambda; ffitted := false; end; function LinearRegression.Fit(X: Matrix; y: Vector): IModel; begin var n := X.RowCount; - var p := X.ColCount; if n = 0 then raise new Exception('Empty dataset'); @@ -585,14 +626,10 @@ begin XtX := XtX - n * Matrix.OuterProduct(meanX, meanX); XtY := XtY - n * meanX * meanY; - // 4. Ridge regularization - if flambda > 0 then - XtX := XtX + Matrix.Identity(p) * flambda; - - // 5. Solve + // 4. Solve fcoef := Solve(XtX, XtY); - // 6. Intercept + // 5. Intercept fintercept := meanY - meanX.Dot(fcoef); ffitted := true; @@ -610,6 +647,17 @@ begin Result := X * fcoef + intercept; end; +function LinearRegression.ToString: string; +begin + Result := 'LinearRegression' +end; + +function LinearRegression.Clone: IModel; +begin + Result := new LinearRegression(); +end; + + //----------------------------- // Activations //----------------------------- @@ -700,6 +748,18 @@ begin Result := Predict(X, 0.5); end; +function LogisticRegression.ToString: string; +begin + Result := 'LogisticRegression(lambda=' + fLambda + + ', lr=' + fLearningRate + ', epochs=' + fEpochs + ')'; +end; + +function LogisticRegression.Clone: IModel; +begin + Result := new LogisticRegression(fLambda, fLearningRate, fEpochs); +end; + + //----------------------------- // RidgeRegression //----------------------------- @@ -755,6 +815,21 @@ begin Result[i] += fIntercept; end; +function RidgeRegression.ToString: string; +begin + Result := 'RidgeRegression(lambda=' + fLambda + ')'; +end; + +function RidgeRegression.Clone: IModel; +begin + Result := new RidgeRegression(fLambda); +end; + + +//----------------------------- +// ElasticNet +//----------------------------- + constructor ElasticNet.Create(lambda1, lambda2: real; maxIter: integer; tol: real); begin if (lambda1 < 0) or (lambda2 < 0) then @@ -853,6 +928,24 @@ begin Result[i] += fIntercept; end; +function ElasticNet.ToString: string; +begin + Result := + 'ElasticNet(lambda1=' + fLambda1 + + ', lambda2=' + fLambda2 + + ', maxIter=' + fMaxIter + + ', tol=' + fTol + ')'; +end; + +function ElasticNet.Clone: IModel; +begin + Result := new ElasticNet(fLambda1, fLambda2, fMaxIter, fTol); +end; + +//----------------------------- +// MulticlassLogisticRegression +//----------------------------- + constructor MulticlassLogisticRegression.Create(lambda: real; lr: real; epochs: integer); begin fLambda := lambda; @@ -940,7 +1033,6 @@ begin end; end; - function MulticlassLogisticRegression.PredictProbaMatrix(X: Matrix): Matrix; begin if not fFitted then @@ -984,6 +1076,20 @@ begin Result[i] := P.RowArgMax(i); end; +function MulticlassLogisticRegression.ToString: string; +begin + Result := + 'MulticlassLogisticRegression(lambda=' + fLambda + + ', lr=' + fLearningRate + + ', epochs=' + fEpochs + ')'; +end; + +function MulticlassLogisticRegression.Clone: IModel; +begin + Result := new MulticlassLogisticRegression(fLambda, fLearningRate, fEpochs); +end; + + //----------------------------- // Pipeline //----------------------------- @@ -1004,12 +1110,31 @@ begin fModel := model; end; -static function Pipeline.Build(model: IModel; transformers: array of ITransformer): Pipeline; +class function Pipeline.Build(params steps: array of IPipeStep): Pipeline; begin - Result := new Pipeline(model); + if (steps = nil) or (Length(steps) = 0) then + raise new ArgumentException('Pipeline requires at least one step'); - foreach var t in transformers do - Result.Add(t); + // последний шаг должен быть моделью + var last := steps[High(steps)]; + + if not (last is IModel) then + raise new ArgumentException('Last step must be a model (IModel)'); + + var pipe := new Pipeline(last as IModel); + + // все шаги кроме последнего должны быть преобразователями + for var i := 0 to High(steps) - 1 do + begin + var step := steps[i]; + + if not (step is ITransformer) then + raise new ArgumentException('All steps except the last must be transformers'); + + pipe.Add(step as ITransformer); + end; + + Result := pipe; end; function Pipeline.Add(t: ITransformer): Pipeline; @@ -1087,6 +1212,40 @@ begin Result := (fModel as IProbabilisticClassifier).PredictProba(Xt); end; +function Pipeline.ToString: string; +begin + var sb := 'Pipeline (' + + (if fFitted then 'trained' else 'not trained') + '):' + NewLine; + + var idx := 1; + + foreach var t in fTransformers do + begin + sb += ' [' + idx + '] ' + t.ToString + NewLine; + idx += 1; + end; + + if fModel <> nil then + sb += ' [' + idx + '] ' + fModel.ToString; + + Result := sb; +end; + +function Pipeline.Clone: IModel; +begin + if fModel = nil then + raise new InvalidOperationException('Pipeline has no model'); + + var p := new Pipeline; + + foreach var t in fTransformers do + p.Add(t.Clone); + + p.SetModel(fModel.Clone); + + Result := p; +end; + //----------------------------- // StandardScaler //----------------------------- @@ -1124,6 +1283,17 @@ begin end; end; +function StandardScaler.ToString: string; +begin + Result := 'StandardScaler'; +end; + +function StandardScaler.Clone: ITransformer; +begin + Result := new StandardScaler(); +end; + + //----------------------------- // MinMaxScaler //----------------------------- @@ -1174,6 +1344,17 @@ begin end; end; +function MinMaxScaler.ToString: string; +begin + Result := 'MinMaxScaler(min=' + fRangeMin + ', max=' + fRangeMax + ')'; +end; + +function MinMaxScaler.Clone: ITransformer; +begin + Result := new MinMaxScaler(fRangeMin, fRangeMax); +end; + + //----------------------------- // PCATransformer //----------------------------- @@ -1222,6 +1403,16 @@ begin Result := Xc * fComponents; end; +function PCATransformer.ToString: string; +begin + Result := 'PCATransformer(k=' + fK + ')'; +end; + +function PCATransformer.Clone: ITransformer; +begin + Result := new PCATransformer(fK); +end; + //----------------------------- // VarianceThreshold //----------------------------- @@ -1268,6 +1459,16 @@ begin Result := R; end; +function VarianceThreshold.ToString: string; +begin + Result := 'VarianceThreshold(threshold=' + fThreshold + ')'; +end; + +function VarianceThreshold.Clone: ITransformer; +begin + Result := new VarianceThreshold(fThreshold); +end; + //----------------------------- // SelectKBest //----------------------------- @@ -1490,6 +1691,26 @@ begin Result := R; end; +function SelectKBest.ToString: string; +begin + var scoreStr := + if fScoreFunc <> nil then + 'custom' + else + fScoreType.ToString; + + Result := 'SelectKBest(k=' + fK + ', score=' + scoreStr + ')'; +end; + +function SelectKBest.Clone: ITransformer; +begin + if fScoreFunc <> nil then + Result := new SelectKBest(fK, fScoreFunc) + else + Result := new SelectKBest(fK, fScoreType); +end; + + //----------------------------- // Normalizer //----------------------------- @@ -1544,6 +1765,15 @@ begin Result := R; end; +function Normalizer.ToString: string; +begin + Result := + 'Normalizer(norm=' + fNormType.ToString + ')'; +end; - +function Normalizer.Clone: ITransformer; +begin + Result := new Normalizer(fNormType); +end; + end. \ No newline at end of file diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index dd3233e3b..e32885714 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -49,6 +49,30 @@ type /// 0.5 — случайная модель, /// 1 — идеальная модель. static function AUC(yTrue, yProb: Vector): real; + + /// Доля правильных предсказаний. + /// Показывает, какая часть объектов классифицирована верно. + /// Удобна, когда классы примерно сбалансированы. + /// Пример: Accuracy = 0.9 означает 90% правильных ответов. + static function Accuracy(yTrue, yPred: Vector): real; + + /// Точность (Precision) для положительного класса (1). + /// Среди всех объектов, которые модель предсказала как 1, + /// показывает долю действительно принадлежащих этому классу. + /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). + static function Precision(yTrue, yPred: Vector): real; + + /// Полнота (Recall) для положительного класса (1). + /// Среди всех объектов, которые в действительности равны 1, + /// показывает долю правильно найденных моделью. + /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). + static function Recall(yTrue, yPred: Vector): real; + + /// F1-мера. + /// Объединённая метрика, учитывающая и Precision, и Recall одновременно. + /// Высока только тогда, когда и Precision, и Recall высоки. + /// Используется, когда классы сильно различаются по количеству элементов + static function F1(yTrue, yPred: Vector): real; end; type @@ -267,6 +291,27 @@ begin Result := area; end; +static function Metrics.Accuracy(yTrue, yPred: Vector): real; +begin + Result := ConfusionMatrix.Create(yTrue, yPred).Accuracy; +end; + +static function Metrics.Precision(yTrue, yPred: Vector): real; +begin + Result := ConfusionMatrix.Create(yTrue, yPred).Precision; +end; + +static function Metrics.Recall(yTrue, yPred: Vector): real; +begin + Result := ConfusionMatrix.Create(yTrue, yPred).Recall; +end; + +static function Metrics.F1(yTrue, yPred: Vector): real; +begin + Result := ConfusionMatrix.Create(yTrue, yPred).F1; +end; + + //----------------------------- // ConfusionMatrix //----------------------------- diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index 8ebfb6944..a20233cd3 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -57,18 +57,21 @@ type /// X, y — обучающие данные. /// k — число фолдов в кросс-валидации. /// metric — функция оценки качества (yTrue, yPred) → real. - /// Возвращает кортеж (лучший параметр, лучшее среднее значение метрики). - class function Search( - modelFactory: real -> IModel; - paramValues: array of real; - X: Matrix; y: Vector; - k: integer; - metric: (Vector, Vector) -> real - ): (real, real); where T: IModel; + /// Возвращает кортеж (лучший параметр, лучшее среднее значение метрики, + /// модель, обученная на всём датасете с лучшим параметром). + class function Search( + modelFactory: real -> T; + paramValues: array of real; + X: Matrix; y: Vector; + k: integer; + metric: (Vector, Vector) -> real + ): (real, real, T); where T: IModel; end; implementation +uses System; + //----------------------------- // Validation //----------------------------- @@ -235,7 +238,8 @@ begin yte[i] := y[r]; end; - var m := model.Fit(Xtr, ytr); + var m := model.Clone(); + m.Fit(Xtr, ytr); var pred := m.Predict(Xte); total += metric(yte, pred); @@ -281,7 +285,8 @@ begin yte[i] := y[r]; end; - var m := model.Fit(Xtr, ytr); + var m := model.Clone(); + m.Fit(Xtr, ytr); var pred := m.Predict(Xte); total += metric(yte, pred); @@ -296,65 +301,23 @@ end; //----------------------------- class function GridSearch.Search( - modelFactory: real -> IModel; + modelFactory: real -> T; paramValues: array of real; X: Matrix; y: Vector; k: integer; metric: (Vector, Vector) -> real -): (real, real); where T: IModel; +): (real, real, T); where T: IModel; begin - var bestParam := 0.0; - var bestScore := -1e308; + if paramValues.Length = 0 then + raise new ArgumentException('paramValues is empty'); - var n := X.RowCount; - var foldSize := n div k; + var bestParam := paramValues[0]; + var bestScore := -1e308; foreach var param in paramValues do begin - var totalScore := 0.0; - - for var fold := 0 to k - 1 do - begin - var startIdx := fold * foldSize; - var endIdx := if fold = k - 1 then n - 1 else (startIdx + foldSize - 1); - - var trainCount := n - (endIdx - startIdx + 1); - var testCount := endIdx - startIdx + 1; - - var Xtrain := new Matrix(trainCount, X.ColCount); - var ytrain := new Vector(trainCount); - var Xtest := new Matrix(testCount, X.ColCount); - var ytest := new Vector(testCount); - - var ti := 0; - var si := 0; - - for var i := 0 to n - 1 do - begin - if (i >= startIdx) and (i <= endIdx) then - begin - for var j := 0 to X.ColCount - 1 do - Xtest[si,j] := X[i,j]; - ytest[si] := y[i]; - si += 1; - end - else - begin - for var j := 0 to X.ColCount - 1 do - Xtrain[ti,j] := X[i,j]; - ytrain[ti] := y[i]; - ti += 1; - end; - end; - - var model := modelFactory(param); - model.Fit(Xtrain, ytrain); - - var pred := model.Predict(Xtest); - totalScore += metric(ytest, pred); - end; - - var avgScore := totalScore / k; + var model := modelFactory(param); + var avgScore := Validation.CrossValidate(model, X, y, k, metric); if avgScore > bestScore then begin @@ -363,8 +326,12 @@ begin end; end; - Result := (bestParam, bestScore); + var bestModel := modelFactory(bestParam); + bestModel.Fit(X, y); + + Result := (bestParam, bestScore, bestModel); end; + end.