ML - небольшой рефакторинг

This commit is contained in:
Mikhalkovich Stanislav 2026-02-17 16:18:17 +03:00
parent 98fe38141b
commit c99dcd30b8
5 changed files with 343 additions and 91 deletions

View file

@ -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;

View file

@ -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;
/// Интерфейс классификатора.
@ -38,7 +44,7 @@ type
/// Базовый интерфейс преобразования признаков.
/// Используется для масштабирования, отбора,
/// уменьшения размерности и других преобразований данных.
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

View file

@ -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.

View file

@ -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
//-----------------------------

View file

@ -57,18 +57,21 @@ type
/// X, y — обучающие данные.
/// k — число фолдов в кросс-валидации.
/// metric — функция оценки качества (yTrue, yPred) → real.
/// Возвращает кортеж (лучший параметр, лучшее среднее значение метрики).
class function Search<T>(
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<T>(
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<T>(
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.