Добавлены примеры ML

Улучшены параметры синтетических датасетов

Метрики разнесены по трем классам

PlotML - сделан Surface и улучшены палитры
This commit is contained in:
Mikhalkovich Stanislav 2026-03-24 14:26:08 +03:00
parent 750902d09f
commit 7b1f89fb4c
21 changed files with 2018 additions and 199 deletions

View file

@ -0,0 +1,40 @@
// Простейший пример линейной регрессии на синтетических данных.
//
// Датасет сгенерирован функцией MakeRegression:
// • признаки — случайные вещественные значения
// • зависимость — линейная комбинация признаков
// • добавлен небольшой гауссов шум
// • нелинейность отключена (nonlinearStrength = 0)
//
// Таким образом, модель полностью соответствует предположениям LinearRegression.
//
// Ожидаемый результат:
// • модель восстанавливает зависимость почти идеально
// • ошибка (MSE) близка к нулю
//
// Этот пример демонстрирует:
// • базовый pipeline регрессии (Fit → Predict → Metrics)
// • ситуацию "идеальной модели", когда данные линейны
// • нижнюю границу ошибки для линейных методов
uses MLABC;
begin
var (X, y) := Datasets.MakeRegression(
n := 300,
nFeatures := 10,
nInformative := 8,
noise := 0.1,
coefScale := 1.0,
bias := 0.0,
nonlinearStrength := 0.0,
shuffle := True,
seed := 42
);
var model := new LinearRegression;
model.Fit(X, y);
var yPred := model.Predict(X);
Println('LinearRegression MSE:', Metrics.MSE(y, yPred):0:4);
end.

View file

@ -0,0 +1,91 @@
// Пример влияния регуляризации (RidgeRegression) на линейную модель
// в задаче с большим числом признаков и относительно малым числом объектов.
//
// Датасет:
// • 20 признаков, из которых информативны только 3, остальные — шум
// • добавлен существенный гауссов шум (noise = 2.0)
// • зависимость остаётся линейной, но сигнал частично скрыт шумом
//
// Такая постановка приводит к переобучению:
// модель может подстраиваться под шум и случайные колебания признаков.
//
// Наблюдения:
//
// • LinearRegression:
// – достигает меньшей ошибки на обучающей выборке (лучше подгонка)
// – но заметно хуже работает на тесте (переобучение)
//
// • RidgeRegression:
// – даёт большую ошибку на обучении (штраф за коэффициенты)
// – но существенно лучше обобщает и снижает ошибку на тесте
//
// Интерпретация:
//
// • обычная линейная регрессия переобучается на шуме и лишних признаках
// • Ridge вводит L2-регуляризацию и "сжимает" коэффициенты модели
// • регуляризация подавляет влияние неинформативных признаков
// • в результате модель становится более устойчивой к новым данным
//
// Этот пример демонстрирует:
// • разницу между train и test ошибками как признак переобучения
// • влияние лишних признаков на качество модели
// • практическую пользу регуляризации в задачах с шумом и мультиколлинеарностью
uses MLABC;
function Normal(rnd: System.Random): real;
begin
var u1 := rnd.NextDouble;
var u2 := rnd.NextDouble;
if u1 < 1e-12 then
u1 := 1e-12;
Result := Sqrt(-2 * Ln(u1)) * Cos(2 * Pi * u2);
end;
begin
var (X, y) := Datasets.MakeRegression(
n := 100,
nFeatures := 20,
nInformative := 3,
noise := 2.0,
coefScale := 1.0,
bias := 0.0,
nonlinearStrength := 0.0,
shuffle := True,
seed := 42
);
// --- добавляем корреляцию
var rnd := new System.Random(1);
for var i := 0 to X.RowCount - 1 do
begin
X[i, 1] := X[i, 0] + 0.01 * Normal(rnd);
X[i, 2] := X[i, 0] - 0.01 * Normal(rnd);
end;
var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(
X, y,
testRatio := 0.3,
seed := 42
);
var lr := new LinearRegression;
lr.Fit(XTrain, yTrain);
var ridge := new RidgeRegression(50.0);
ridge.Fit(XTrain, yTrain);
var yTrainLR := lr.Predict(XTrain);
var yTestLR := lr.Predict(XTest);
var yTrainRidge := ridge.Predict(XTrain);
var yTestRidge := ridge.Predict(XTest);
Println('--- Train ---');
Println($'Linear MSE: {Metrics.MSE(yTrain, yTrainLR),0:F4}');
Println($'Ridge MSE: {Metrics.MSE(yTrain, yTrainRidge),0:F4}');
Println;
Println('--- Test ---');
Println($'Linear MSE: {Metrics.MSE(yTest, yTestLR),0:F4}');
Println($'Ridge MSE: {Metrics.MSE(yTest, yTestRidge),0:F4}');
end.

View file

@ -0,0 +1,90 @@
// В этом примере сравниваются три модели: LinearRegression, Ridge и ElasticNet.
// На обучении Linear даёт наименьшую ошибку, так как максимально подгоняет данные,
// но на тесте его ошибка резко растёт — это признак переобучения. Ridge слегка
// ухудшает качество на train, зато заметно снижает ошибку на test за счёт
// регуляризации (штрафа на величину коэффициентов). ElasticNet ведёт себя как
// компромисс: он тоже борется с переобучением и дополнительно может занулять
// часть коэффициентов, выполняя отбор признаков.
// По коэффициентам это видно особенно хорошо: у Linear они большие и нестабильные
// (из-за коррелированных признаков модель «раздувает» веса и компенсирует одни
// признаки другими). Ridge сглаживает веса — они становятся меньше и ближе друг
// к другу, что делает решение устойчивым. ElasticNet помимо сглаживания ещё и
// зануляет часть коэффициентов, оставляя только наиболее значимые признаки.
// Итог: Linear лучше подгоняет обучение, но переобучается; Ridge даёт лучший
// баланс и результат на тесте; ElasticNet — промежуточный вариант с эффектом
// отбора признаков.
uses MLABC;
uses System;
function Normal(rnd: Random): real;
begin
var u1 := rnd.NextDouble;
var u2 := rnd.NextDouble;
if u1 < 1e-12 then u1 := 1e-12;
Result := Sqrt(-2 * Ln(u1)) * Cos(2 * Pi * u2);
end;
begin
var (X, y) := Datasets.MakeRegression(
n := 100,
nFeatures := 20,
nInformative := 3,
noise := 2.0,
coefScale := 1.0,
bias := 0.0,
nonlinearStrength := 0.0,
shuffle := True,
seed := 42
);
// --- корреляция
var rnd := new Random(1);
for var i := 0 to X.RowCount - 1 do
begin
X[i, 1] := X[i, 0] + 0.01 * Normal(rnd);
X[i, 2] := X[i, 0] - 0.01 * Normal(rnd);
end;
var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(
X, y, testRatio := 0.3, seed := 42
);
var lr := new LinearRegression;
lr.Fit(XTrain, yTrain);
var ridge := new RidgeRegression(50.0);
ridge.Fit(XTrain, yTrain);
var en := new ElasticNet(5.0, 0.2); // alpha, l1_ratio
en.Fit(XTrain, yTrain);
// --- предсказания
var yTrainLR := lr.Predict(XTrain);
var yTestLR := lr.Predict(XTest);
var yTrainRidge := ridge.Predict(XTrain);
var yTestRidge := ridge.Predict(XTest);
var yTrainEN := en.Predict(XTrain);
var yTestEN := en.Predict(XTest);
// --- вывод
Println('--- Train ---');
Println($'Linear MSE: {Metrics.MSE(yTrain, yTrainLR),0:F4}');
Println($'Ridge MSE: {Metrics.MSE(yTrain, yTrainRidge),0:F4}');
Println($'ElasticNet MSE: {Metrics.MSE(yTrain, yTrainEN),0:F4}');
Println;
Println('--- Test ---');
Println($'Linear MSE: {Metrics.MSE(yTest, yTestLR),0:F4}');
Println($'Ridge MSE: {Metrics.MSE(yTest, yTestRidge),0:F4}');
Println($'ElasticNet MSE: {Metrics.MSE(yTest, yTestEN),0:F4}');
{Println;
Println('Коэффициенты (первые 10):');
lr.Coefficients.ToArray.Take(10).Println;
ridge.Coefficients.ToArray.Take(10).Println;
en.Coefficients.ToArray.Take(10).Println;}
end.

View file

@ -0,0 +1,60 @@
// В этом примере зависимость между признаками и целевой переменной
// специально сделана нелинейной. Линейная регрессия не умеет
// аппроксимировать такую зависимость, поэтому даёт большую ошибку
// и на обучении, и на тесте. Это не переобучение, а недообучение:
// модель слишком простая для задачи.
// RandomForest и GradientBoosting работают намного лучше,
// потому что деревья решений умеют моделировать сложные нелинейные связи.
// GradientBoosting показывает лучший результат,
// так как особенно хорошо приближает сложные зависимости.
uses MLABC;
begin
var (X, y) := Datasets.MakeRegression(
n := 300,
nFeatures := 2,
nInformative := 2,
noise := 0.1,
coefScale := 1.0,
bias := 0.0,
nonlinearStrength := 5.0, // КЛЮЧЕВОЕ !!!
shuffle := True,
seed := 42
);
var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(
X, y, testRatio := 0.3, seed := 42
);
var lr := new LinearRegression;
lr.Fit(XTrain, yTrain);
var rf := new RandomForestRegressor(100);
rf.Fit(XTrain, yTrain);
var gb := new GradientBoostingRegressor(100);
gb.Fit(XTrain, yTrain);
var yTrainLR := lr.Predict(XTrain);
var yTestLR := lr.Predict(XTest);
var yTrainRF := rf.Predict(XTrain);
var yTestRF := rf.Predict(XTest);
var yTrainGB := gb.Predict(XTrain);
var yTestGB := gb.Predict(XTest);
Println('nonlinearStrength = 5.0');
Println;
Println('--- Train ---');
Println($'Linear MSE: {RegressionMetrics.MSE(yTrain, yTrainLR),0:F4}');
Println($'Forest MSE: {RegressionMetrics.MSE(yTrain, yTrainRF),0:F4}');
Println($'Boost MSE: {RegressionMetrics.MSE(yTrain, yTrainGB),0:F4}');
Println;
Println('--- Test ---');
Println($'Linear MSE: {RegressionMetrics.MSE(yTest, yTestLR),0:F4}');
Println($'Forest MSE: {RegressionMetrics.MSE(yTest, yTestRF),0:F4}');
Println($'Boost MSE: {RegressionMetrics.MSE(yTest, yTestGB),0:F4}');
end.

View file

@ -0,0 +1,74 @@
// В датасете MakeMoons классы разделяются нелинейной границей.
// Поэтому LogisticRegression, строящая линейную разделяющую поверхность,
// показывает худший результат.
// DecisionTreeClassifier идеально подгоняет обучающую выборку,
// но на тесте заметно уступает, что указывает на переобучение.
// RandomForest и GradientBoosting значительно лучше работают
// с нелинейной структурой данных за счёт ансамблей деревьев.
// KNNClassifier показывает лучший результат на тесте,
// потому что это локальная модель, хорошо улавливающая
// сложную геометрию классов.
uses MLABC;
begin
var (X, y) := Datasets.MakeMoons(
n := 300,
noise := 0.2,
shuffle := True,
seed := 42
);
var (XTrain, XTest, yTrain, yTest) := Validation.TrainTestSplit(
X, y, testRatio := 0.3, seed := 42
);
var logreg := new LogisticRegression;
logreg.Fit(XTrain, yTrain);
var tree := new DecisionTreeClassifier;
tree.Fit(XTrain, yTrain);
var rf := new RandomForestClassifier(100);
rf.Fit(XTrain, yTrain);
var gb := new GradientBoostingClassifier(100);
gb.Fit(XTrain, yTrain);
var knn := new KNNClassifier(5);
knn.Fit(XTrain, yTrain);
// --- предсказания
var yTrainLR := logreg.Predict(XTrain);
var yTestLR := logreg.Predict(XTest);
var yTrainTree := tree.Predict(XTrain);
var yTestTree := tree.Predict(XTest);
var yTrainRF := rf.Predict(XTrain);
var yTestRF := rf.Predict(XTest);
var yTrainGB := gb.Predict(XTrain);
var yTestGB := gb.Predict(XTest);
var yTrainKNN := knn.Predict(XTrain);
var yTestKNN := knn.Predict(XTest);
// --- вывод accuracy
Println('--- Train ---');
Println($'Logistic Acc: {Metrics.Accuracy(yTrain, yTrainLR),0:F4}');
Println($'Tree Acc: {Metrics.Accuracy(yTrain, yTrainTree),0:F4}');
Println($'Forest Acc: {Metrics.Accuracy(yTrain, yTrainRF),0:F4}');
Println($'Boost Acc: {Metrics.Accuracy(yTrain, yTrainGB),0:F4}');
Println($'KNN Acc: {Metrics.Accuracy(yTrain, yTrainKNN),0:F4}');
Println;
Println('--- Test ---');
Println($'Logistic Acc: {Metrics.Accuracy(yTest, yTestLR),0:F4}');
Println($'Tree Acc: {Metrics.Accuracy(yTest, yTestTree),0:F4}');
Println($'Forest Acc: {Metrics.Accuracy(yTest, yTestRF),0:F4}');
Println($'Boost Acc: {Metrics.Accuracy(yTest, yTestGB),0:F4}');
Println($'KNN Acc: {Metrics.Accuracy(yTest, yTestKNN),0:F4}');
end.

View file

@ -0,0 +1,89 @@
// Линейно разделимая задача с шумом.
// LogisticRegression показывает наилучший результат,
// так как модель соответствует природе данных.
//
// DecisionTree переобучается (рваная граница) → хуже на тесте.
// RandomForest снижает переобучение и приближается к Logistic.
// KNN чувствителен к шуму и даёт промежуточный результат.
//
// Logistic Acc: 0.8733
// Tree Acc: 0.7800
// Forest Acc: 0.8467
// KNN Acc: 0.8533
uses MLABC, PlotML;
begin
// --- данные
var (X, y) := Datasets.MakeClassification(
n := 500,
nFeatures := 2,
nInformative := 2,
nRedundant := 0,
noise := 0.5,
classSep := 2.0,
flipProb := 0.1,
classBalance := 0.5,
shuffle := True,
seed := 1
);
var (XTrain, XTest, yTrain, yTest) :=
Validation.TrainTestSplit(X, y, testRatio := 0.3, seed := 1);
// --- модели
var logreg := new LogisticRegression;
logreg.Fit(XTrain, yTrain);
var tree := new DecisionTreeClassifier;
tree.Fit(XTrain, yTrain);
var rf := new RandomForestClassifier(100);
rf.Fit(XTrain, yTrain);
var knn := new KNNClassifier(5);
knn.Fit(XTrain, yTrain);
// --- предсказания (для визуализации)
var yLR := logreg.PredictLabels(X);
var yTree := tree.PredictLabels(X);
var yRF := rf.PredictLabels(X);
var yKNN := knn.PredictLabels(X);
// --- test предсказания
var yLR_test := logreg.PredictLabels(XTest);
var yTree_test := tree.PredictLabels(XTest);
var yRF_test := rf.PredictLabels(XTest);
var yKNN_test := knn.PredictLabels(XTest);
// --- метрики
var accLR := Metrics.Accuracy(yTest, yLR_test);
var accTree := Metrics.Accuracy(yTest, yTree_test);
var accRF := Metrics.Accuracy(yTest, yRF_test);
var accKNN := Metrics.Accuracy(yTest, yKNN_test);
Println($'Logistic Acc: {accLR,0:F4}');
Println($'Tree Acc: {accTree,0:F4}');
Println($'Forest Acc: {accRF,0:F4}');
Println($'KNN Acc: {accKNN,0:F4}');
Println;
// --- координаты
var x1 := X.Col(0);
var x2 := X.Col(1);
// --- визуализация
var fig := Plot.Grid(2, 2);
fig[0,0].Points(x1, x2, yLR, size := 5);
fig[0,0].Title := $'Logistic (acc={accLR,0:F3})';
fig[0,1].Points(x1, x2, yTree, size := 5);
fig[0,1].Title := $'Tree (acc={accTree,0:F3})';
fig[1,0].Points(x1, x2, yRF, size := 5);
fig[1,0].Title := $'Forest (acc={accRF,0:F3})';
fig[1,1].Points(x1, x2, yKNN, size := 5);
fig[1,1].Title := $'KNN (acc={accKNN,0:F3})';
end.

View file

@ -0,0 +1,91 @@
// Линейно разделимая задача с шумом.
// Классы в целом разделяются почти прямой линией, но из-за шума
// и случайных выбросов граница получается неидеальной.
//
// LogisticRegression показывает наилучший результат,
// так как её линейная модель хорошо соответствует структуре данных.
//
// DecisionTree строит кусочно-постоянную (рваную) границу,
// переобучается на шуме и даёт худшее качество на тесте.
//
// RandomForest сглаживает переобучение дерева за счёт ансамбля,
// но всё равно уступает линейной модели на этой задаче.
//
// KNN хорошо улавливает локальную структуру,
// но чувствителен к шуму и поэтому даёт промежуточный результат.
//
// Визуализация показывает:
// • Logistic — почти прямая граница
// • Tree — рваная, переобученная
// • Forest — сглаженная версия дерева
// • KNN — локально адаптивная граница
uses MLABC, PlotML;
begin
// --- данные
var (X, y) := Datasets.MakeClassification(
n := 500,
nFeatures := 2,
nInformative := 2,
nRedundant := 0,
noise := 0.5,
classSep := 2.0,
flipProb := 0.1,
classBalance := 0.5,
shuffle := True,
seed := 1
);
var (XTrain, XTest, yTrain, yTest) :=
Validation.TrainTestSplit(X, y, testRatio := 0.3, seed := 1);
// --- модели
var logreg := new LogisticRegression;
logreg.Fit(XTrain, yTrain);
var tree := new DecisionTreeClassifier;
tree.Fit(XTrain, yTrain);
var rf := new RandomForestClassifier(100);
rf.Fit(XTrain, yTrain);
var knn := new KNNClassifier(5);
knn.Fit(XTrain, yTrain);
// --- accuracy на test
var accLR := Metrics.Accuracy(yTest, logreg.PredictLabels(XTest));
var accTree := Metrics.Accuracy(yTest, tree.PredictLabels(XTest));
var accRF := Metrics.Accuracy(yTest, rf.PredictLabels(XTest));
var accKNN := Metrics.Accuracy(yTest, knn.PredictLabels(XTest));
// --- область рисования
var x1 := X.Col(0);
var x2 := X.Col(1);
var yArr := y.ToIntArray;
var nx := 80;
var ny := 80;
// --- визуализация
var fig := Plot.Grid(2, 2);
// Logistic
fig[0,0].Surface(x1, x2, nx, ny, G -> logreg.PredictLabels(G),Palettes.Pastel);
fig[0,0].Points(x1, x2, yArr, size := 6);
fig[0,0].Title := $'Logistic (acc={accLR,0:F3})';
// Tree
fig[0,1].Surface(x1, x2, nx, ny, G -> tree.PredictLabels(G),Palettes.Pastel);
fig[0,1].Points(x1, x2, yArr, size := 6);
fig[0,1].Title := $'Tree (acc={accTree,0:F3})';
// Forest
fig[1,0].Surface(x1, x2, nx, ny, G -> rf.PredictLabels(G),Palettes.Pastel);
fig[1,0].Points(x1, x2, yArr, size := 6);
fig[1,0].Title := $'Forest (acc={accRF,0:F3})';
// KNN
fig[1,1].Surface(x1, x2, nx, ny, G -> knn.PredictLabels(G),Palettes.Pastel);
fig[1,1].Points(x1, x2, yArr, size := 6);
fig[1,1].Title := $'KNN (acc={accKNN,0:F3})';
end.

View file

@ -0,0 +1,97 @@
// Задача с концентрическими окружностями (MakeCircles).
// Классы вложены друг в друга, поэтому линейная разделяющая
// граница в принципе невозможна.
//
// LogisticRegression строит только линейную границу,
// поэтому полностью проваливается (сильное недообучение).
//
// DecisionTree способен моделировать нелинейную структуру,
// но делает это через разбиения по осям, из-за чего граница
// получается рваной и переобученной.
//
// RandomForest усредняет множество деревьев,
// сглаживает границу и даёт более устойчивый результат.
//
// KNNClassifier показывает лучший результат,
// так как является локальной моделью и хорошо восстанавливает
// форму вложенных областей.
//
// Визуализация наглядно показывает различия:
// • Logistic — почти прямая граница (не подходит)
// • Tree — "ступенчатая" структура (переобучение)
// • Forest — сглаженная версия дерева
// • KNN — наиболее близкая к истинной геометрии граница
uses MLABC, PlotML;
begin
// --- данные
var (X, y) := Datasets.MakeCircles(
n := 400,
noise := 0.2,
factor := 0.5,
classBalance := 0.5,
flipProb := 0.05,
scale := 3.0,
shuffle := True,
seed := 1
);
var (XTrain, XTest, yTrain, yTest) :=
Validation.TrainTestSplit(X, y, testRatio := 0.3, seed := 1);
// --- модели
var logreg := new LogisticRegression;
logreg.Fit(XTrain, yTrain);
var tree := new DecisionTreeClassifier(seed := 1);
tree.Fit(XTrain, yTrain);
var rf := new RandomForestClassifier(100, seed := 1);
rf.Fit(XTrain, yTrain);
var knn := new KNNClassifier(5);
knn.Fit(XTrain, yTrain);
// --- метрики
var accLR := Metrics.Accuracy(yTest, logreg.PredictLabels(XTest));
var accTree := Metrics.Accuracy(yTest, tree.PredictLabels(XTest));
var accRF := Metrics.Accuracy(yTest, rf.PredictLabels(XTest));
var accKNN := Metrics.Accuracy(yTest, knn.PredictLabels(XTest));
Println('--- Circles ---');
Println($'Logistic Acc: {accLR,0:F4}');
Println($'Tree Acc: {accTree,0:F4}');
Println($'Forest Acc: {accRF,0:F4}');
Println($'KNN Acc: {accKNN,0:F4}');
// --- данные для визуализации
var x1 := X.Col(0);
var x2 := X.Col(1);
var yArr := y.ToIntArray;
var nx := 80;
var ny := 80;
// --- визуализация
var fig := Plot.Grid(2, 2);
// Logistic
fig[0,0].Surface(x1, x2, nx, ny, G -> logreg.PredictLabels(G),Palettes.Pastel);
fig[0,0].Points(x1, x2, yArr, size := 6);
fig[0,0].Title := $'Logistic (acc={accLR,0:F3})';
// Tree
fig[0,1].Surface(x1, x2, nx, ny, G -> tree.PredictLabels(G), Palettes.Pastel);
fig[0,1].Points(x1, x2, yArr, size := 6);
fig[0,1].Title := $'Tree (acc={accTree,0:F3})';
// Forest
fig[1,0].Surface(x1, x2, nx, ny, G -> rf.PredictLabels(G), Palettes.Pastel);
fig[1,0].Points(x1, x2, yArr, size := 6);
fig[1,0].Title := $'Forest (acc={accRF,0:F3})';
// KNN
fig[1,1].Surface(x1, x2, nx, ny, G -> knn.PredictLabels(G), Palettes.Pastel);
fig[1,1].Points(x1, x2, yArr, size := 6);
fig[1,1].Title := $'KNN (acc={accKNN,0:F3})';
end.

View file

@ -0,0 +1,83 @@
// Сравнение KMeans и DBSCAN на данных с шумом и разной плотностью кластеров.
//
// Датасет содержит:
// • несколько кластеров
// • различную плотность
// • дополнительные шумовые точки
//
// KMeans:
// • предполагает компактные и примерно одинаковые по размеру кластеры
// • плохо работает при наличии шума и различной плотности
// • стремится "размазать" шум по кластерам
//
// DBSCAN:
// • выделяет кластеры как области высокой плотности
// • естественно отделяет шум (метка -1)
// • лучше справляется с неоднородной структурой данных
//
// По метрике ARI DBSCAN показывает лучший результат,
// так как точнее восстанавливает истинную структуру кластеров.
//
// Этот пример демонстрирует различие двух подходов:
// • центроидный (KMeans)
// • плотностный (DBSCAN)
// KMeans ARI: 0.9073
// DBSCAN ARI: 0.9496
uses MLABC, PlotML;
begin
// --- данные (сложный случай: разная плотность + шум)
var (X, yTrue) := Datasets.MakeBlobs(
n := 400,
centers := 3,
nFeatures := 2,
clusterStd := 0.9,
clusterStdVar := 1.0,
centerBox := 6.0,
classBalance := 1.0,
noisePoints := 60,
shuffle := True,
seed := 1
);
// --- модели
var kmeans := new KMeans(3);
kmeans.Fit(X);
var yKM := kmeans.PredictLabels(X);
var db := new DBSCAN(0.7, 5);
db.Fit(X);
var yDB := db.PredictLabels(X);
// --- метрики
var ariKM := Metrics.AdjustedRandIndex(yTrue, yKM);
var ariDB := Metrics.AdjustedRandIndex(yTrue, yDB);
Println('--- Clustering ---');
Println($'KMeans ARI: {ariKM,0:F4}');
Println($'DBSCAN ARI: {ariDB,0:F4}');
Println;
// --- координаты
var x1 := X.Col(0);
var x2 := X.Col(1);
// --- визуализация
var fig := Plot.Grid(1, 2);
// KMeans
//fig[0,0].SetPalette(Palettes.Bright);
fig[0,0].Points(x1, x2, yKM, size := 6);
fig[0,0].Title := $'KMeans (ARI={ariKM,0:F3})';
var yDB_plot := new integer[yDB.Length];
for var i := 0 to yDB.Length - 1 do
yDB_plot[i] := if yDB[i] = -1 then 0 else yDB[i] + 1;
// DBSCAN
//fig[0,1].SetPalette(Palettes.Bright);
fig[0,1].Points(x1, x2, yDB_plot, size := 6);
fig[0,1].Title := $'DBSCAN (ARI={ariDB,0:F3})';
end.

View file

@ -7,4 +7,6 @@ begin
df.Schema.Println;
df.PrintlnInfo;
df.PrintlnPreview(20);
end.

View file

@ -0,0 +1,38 @@
uses MLABC, PlotML;
begin
var ds := Datasets.RussianCities;
var df := ds.Data;
// --- признаки
df := df.WithColumnFloat('density', row -> row.Float('population') / row.Float('area'));
df := df.WithColumnInt('age', row -> 2026 - row.Int('foundation_year'));
df := df.WithColumnFloat('log_population', row -> Ln(row.Float('population')));
var features := ['log_population', 'density', 'lat', 'lon'{, 'age'}];
// --- кластеризация
var X := df.ToMatrix(features);
var km := new KMeans(5, seed := 42);
km.Fit(X);
var labels: array of integer := km.PredictLabels(X);
df.AddIntColumn('cluster', labels, nil);
// --- координаты
var lon := df.ToVector('lon').ToArray;
var lat := df.ToVector('lat').ToArray;
Plot.Points(
lon,
lat,
labels,
size := 5,
marker := MarkerType.Circle
);
Plot.XLabel('Долгота');
Plot.YLabel('Широта');
Plot.Title := 'Кластеры городов России';
end.

View file

@ -0,0 +1,150 @@
// По log_population, density KMeans выделил три группы городов с разным масштабом и характером.
//
// Кластер 0 — крупнейшие и наиболее значимые города.
// Здесь сосредоточены мегаполисы и крупные региональные центры.
// Для них характерны большое население, сравнительно высокая плотность и в среднем более крупная площадь.
//
// Кластер 1 — компактные, но плотные города.
// Их немного, население у них умеренное, зато площадь очень мала, поэтому плотность населения максимальна.
// Это особая группа "сжатых" городов с городской концентрацией.
//
// Кластер 2 — малые и средние города.
// Это самый массовый кластер.
// Для него характерны небольшое население, низкая плотность и сравнительно умеренная площадь.
// Именно он отражает "обычный" фон большинства российских городов.
uses MLABC;
begin
var ds := Datasets.RussianCities;
var df := ds.Data;
// --- признаки
df := df.WithColumnFloat('density', row -> row.Float('population') / row.Float('area'));
df := df.WithColumnInt('age', row -> 2025 - row.Int('foundation_year'));
df := df.WithColumnFloat('log_population', row -> Ln(row.Float('population')));
var features := ['log_population', 'density'{, 'lat', 'lon', 'age'}];
Println('Подбор числа кластеров для KMeans');
Println;
Println('k inertia silhouette calinski_harabasz davies_bouldin');
Println('-' * 78);
var sils := new List<real>;
var chs := new List<real>;
var dbis := new List<real>;
foreach var k in 2..8 do
begin
var model := new KMeans(k, seed := 42);
var pipe :=
UDataPipeline.Build(
features,
new StandardScaler,
model
);
pipe.Fit(df);
var labels := pipe.PredictLabels(df);
var dfT := pipe.Transform(df);
var X := dfT.ToMatrix(features);
var inertia := model.Inertia;
var sil := Metrics.SilhouetteScore(X, labels);
var ch := Metrics.CalinskiHarabaszScore(X, labels);
var dbi := Metrics.DaviesBouldinScore(X, labels);
sils.Add(sil);
chs.Add(ch);
dbis.Add(dbi);
Println($'{k,-4} {inertia,12:F3} {sil,14:F4} {ch,20:F3} {dbi,18:F4}');
end;
// границы
var sil_min := sils.Min; var sil_max := sils.Max;
var ch_min := chs.Min; var ch_max := chs.Max;
var dbi_min := dbis.Min; var dbi_max := dbis.Max;
// второй проход
var bestScore := -1e9;
var bestK := -1;
for var i := 0 to sils.Count - 1 do
begin
var sil_norm := (sils[i] - sil_min) / (sil_max - sil_min);
var ch_norm := (chs[i] - ch_min) / (ch_max - ch_min);
var dbi_norm := (dbi_max - dbis[i]) / (dbi_max - dbi_min);
var score := 0.5 * sil_norm + 0.3 * ch_norm + 0.2 * dbi_norm;
var k := i + 2;
if score > bestScore then
begin
bestScore := score;
bestK := k;
end;
end;
Println;
Println($'Лучший k: {bestK}');
Println;
var model := new KMeans(bestK, seed := 42);
// --- финальная модель
var pipe :=
UDataPipeline.Build(
features,
new StandardScaler,
model
);
pipe.Fit(df);
var labels := pipe.PredictLabels(df);
var X := df.ToMatrix(features);
df.AddIntColumn('cluster', labels, nil);
Println('Финальные метрики для выбранного k:');
Println($'Inertia: {model.Inertia:F3}');
Println($'SilhouetteScore: {Metrics.SilhouetteScore(X, labels):F4}');
Println($'CalinskiHarabasz: {Metrics.CalinskiHarabaszScore(X, labels):F3}');
Println($'DaviesBouldin: {Metrics.DaviesBouldinScore(X, labels):F4}');
Println;
Println('Центры кластеров (в стандартизованном пространстве признаков):');
model.ClusterCenters.Print;
Println;
// --- анализ кластеров
for var c := 0 to bestK - 1 do
begin
var sub := df.Filter(r -> r.Int('cluster') = c);
Println;
Println('=' * 70);
Println($'Кластер {c}');
Println('=' * 70);
Println($'Число городов: {sub.RowCount}');
Println($'Среднее население: {sub.Mean(''population''),0:F3}');
Println($'Средняя площадь: {sub.Mean(''area''),0:F3}');
Println($'Средняя плотность: {sub.Mean(''density''),0:F3}');
Println($'Средний возраст: {sub.Mean(''age''),0:F3}');
Println;
Println('Топ-10 по населению:');
sub.SortBy('population', descending := True)
.Select(['city', 'population', 'area', 'density', 'age'])
.Head(10)
.Print;
end;
end.

View file

@ -0,0 +1,150 @@
// Подбор параметра eps для DBSCAN с использованием фильтрации и составного скоринга.
//
// Рассматривались различные значения eps и для каждого вычислялись:
// • число кластеров (k)
// • доля крупнейшего кластера (max%)
// • доля шума (noise%)
// • silhouette
//
// Наблюдения:
//
// • При увеличении eps происходит слияние кластеров.
// Уже при eps ≥ 0.6 практически все объекты попадают в один доминирующий кластер
// (max% ≈ 0.97–0.99), что делает разбиение вырожденным.
//
// • Несмотря на высокий silhouette (например, при eps = 0.8),
// такие решения не являются содержательно корректными,
// так как фактически отражают почти однородную структуру данных.
//
// • Фильтрация отбрасывает варианты с:
// – одним кластером,
// – слишком большим доминирующим кластером,
// – некорректным уровнем шума.
//
// • В результате единственным допустимым вариантом оказался eps = 0.2.
//
// Интерпретация результата:
//
// • При eps = 0.2 алгоритм выделяет несколько кластеров (k = 5),
// однако структура получается фрагментированной:
// присутствует один крупный кластер и несколько очень малых,
// а также заметная доля шума.
//
// • Это говорит о том, что:
// – DBSCAN плохо согласуется с геометрией данного датасета,
// – либо признаки недостаточно разделимы по плотности,
// – либо требуется более аккуратный подбор параметров (eps, minPts),
// или использование другой модели кластеризации.
//
// Вывод:
//
// • DBSCAN в данном виде не даёт устойчивого и интерпретируемого разбиения.
// • Жёсткая фильтрация позволяет избежать вырожденных решений,
// но может приводить к выбору некачественного eps.
// • Для данного датасета более предпочтительны методы типа KMeans,
// либо требуется дополнительная предобработка признаков.
uses MLABC;
begin
var ds := Datasets.RussianCities;
var df := ds.Data;
df := df.WithColumnFloat('density', r -> r.Float('population') / r.Float('area'));
df := df.WithColumnFloat('log_population', r -> Ln(r.Float('population')));
df := df.WithColumnInt('age', row -> 2025 - row.Int('foundation_year'));
var features := ['log_population', 'density'];
Println('Подбор eps для DBSCAN (с фильтрацией)');
Println;
Println('eps k max% noise% silhouette score');
Println('-' * 70);
var X := df.ToMatrix(features);
var scaler := new StandardScaler;
scaler.Fit(X);
X := scaler.Transform(X);
var bestEps := -1.0;
var bestScore := -1.0;
foreach var eps in [0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0] do
begin
var model := new DBSCAN(eps, 5);
model.Fit(X);
var labels := model.PredictLabels(X);
var counts := new Dictionary<integer, integer>();
foreach var l in labels do
counts[l] := counts.Get(l, 0) + 1;
var n := df.RowCount;
// --- считаем кластеры (без шума)
var k := 0;
var maxCluster := 0;
var noise := counts.Get(-1, 0);
foreach var kv in counts do
if kv.Key <> -1 then
begin
k += 1;
if kv.Value > maxCluster then
maxCluster := kv.Value;
end;
var maxFrac := maxCluster / n;
var noiseFrac := noise / n;
var sil := -1.0;
if k >= 2 then
sil := Metrics.SilhouetteScore(X, labels);
// --- фильтрация плохих случаев
var valid :=
(k >= 2) and (k <= 5) and
(maxFrac < 0.85) and
(noiseFrac > 0.01) and (noiseFrac < 0.4);
var score := -1.0;
if valid then
begin
// составной скоринг
score :=
sil
- 0.5 * maxFrac // штраф за доминирующий кластер
- 0.3 * noiseFrac; // штраф за шум
if score > bestScore then
begin
bestScore := score;
bestEps := eps;
end;
end;
Println($'{eps,4:F1} {k,4} {maxFrac,7:F2} {noiseFrac,8:F2} {sil,12:F4} {score,10:F4}');
end;
Println;
Println($'Выбран eps: {bestEps,0:F2}');
Println;
// --- финальная модель
var model := new DBSCAN(bestEps, 5);
model.Fit(X);
var labels := model.PredictLabels(X);
df.AddIntColumn('cluster_db', labels, nil);
// --- итог
var counts := new Dictionary<integer, integer>();
foreach var l in labels do
counts[l] := counts.Get(l, 0) + 1;
Println('Финальные кластеры:');
foreach var kv in counts do
Println($'Cluster {kv.Key}: {kv.Value}');
end.

View file

@ -12,5 +12,5 @@ begin
);
Plot.Title('MakeSpiral синтетический датасет');
Plot.Points(X.Col(0), X.Col(1), LabelsToInts(y), size := 3);
Plot.Points(X.Col(0), X.Col(1), LabelsToInts(y), size := 5);
end.

View file

@ -31,6 +31,7 @@ type
function Clone: Vector;
function ToArray: array of real;
function ToIntArray: array of integer;
static function operator +(a, b: Vector): Vector;
static function operator -(a, b: Vector): Vector;
@ -356,6 +357,14 @@ begin
Result := Copy(fdata);
end;
function Vector.ToIntArray: array of integer;
begin
Result := new integer[Length];
for var i := 0 to Length - 1 do
Result[i] := integer(Data[i]);
end;
function Vector.Clone: Vector;
begin
Result := new Vector(fdata);

View file

@ -24,8 +24,11 @@ type
Validation = ValidationML.Validation;
ConfusionMatrix = MetricsABC.ConfusionMatrix;
Metrics = MetricsABC.Metrics;
ClassificationMetrics = MetricsABC.ClassificationMetrics;
RegressionMetrics = MetricsABC.RegressionMetrics;
ClusteringMetrics = MetricsABC.ClusteringMetrics;
ConfusionMatrix = MetricsABC.ConfusionMatrix;
DataPipeline = MLPipelineABC.DataPipeline;

View file

@ -64,6 +64,8 @@ type
/// Наследуется от IModel.
/// Предназначен для моделей, выполняющих классификацию (предсказание меток классов).
IClassifier = interface(ISupervisedModel)
/// Возвращает метки классов
function PredictLabels(X: Matrix): array of integer;
end;
/// Интерфейс классификатора, возвращающего вероятности.

View file

@ -68,20 +68,29 @@ type
static Language: string := 'ru';
// --- Синтетические датасеты (Matrix + Vector)
/// Генерирует синтетический датасет из нескольких гауссовых кластеров.
/// Возвращает матрицу признаков X и вектор меток кластеров y.
/// Генерирует синтетический датасет из гауссовых кластеров.
/// Каждый кластер задаётся центром и разбросом. Используется для задач кластеризации и классификации.
///
/// Параметры:
/// • n — количество объектов (точек)
/// • n — число объектов
/// • centers — число кластеров
/// • clusterStd — стандартное отклонение точек внутри кластера
/// • nFeatures — число признаков (размерность пространства)
/// • seed — значение генератора случайных чисел (seed < 0 → случайный)
/// • nFeatures — размерность пространства
/// • clusterStd — базовое стандартное отклонение
/// • clusterStdVar — разброс std между кластерами (0 → одинаковые)
/// • centerBox — диапазон генерации центров [-centerBox, centerBox]
/// • classBalance — равномерность кластеров (0..1, 1 = равномерно)
/// • noisePoints — число шумовых точек (outliers)
/// • shuffle — перемешивание
/// • seed — генератор (seed < 0 → случайный)
static function MakeBlobs(
n: integer := 300;
centers: integer := 3;
clusterStd: real := 1.0;
nFeatures: integer := 2;
clusterStd: real := 1.0;
clusterStdVar: real := 0.0;
centerBox: real := 5.0;
classBalance: real := 1.0;
noisePoints: integer := 0;
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
@ -99,45 +108,57 @@ type
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
/// Генерирует синтетический датасет для задачи линейной регрессии.
/// Данные создаются по модели y = Xβ + ε, где X — матрица признаков,
/// β — случайный вектор коэффициентов, ε — гауссовский шум.
/// Генерирует синтетический датасет для задачи регрессии.
/// Данные создаются по модели y = Xβ + f(X) + ε, где:
/// X — матрица признаков,
/// β — вектор коэффициентов (часть признаков может быть неинформативной),
/// f(X) — добавочная нелинейная компонента,
/// ε — гауссовский шум.
/// Возвращает матрицу признаков X (n × nFeatures) и вектор целевой переменной y.
///
/// • n — число объектов.
/// • nFeatures — число признаков.
/// • noise — стандартное отклонение гауссовского шума, добавляемого к y.
/// • nFeatures — общее число признаков.
/// • nInformative — число информативных признаков (остальные имеют нулевые коэффициенты).
/// • noise — стандартное отклонение гауссовского шума.
/// • coefScale — масштаб коэффициентов β.
/// • bias — свободный член (смещение).
/// • nonlinearStrength — коэффициент нелинейной компоненты (например, квадрат первого признака).
/// • shuffle — перемешивать ли порядок объектов.
/// • seed — значение генератора случайных чисел (-1 означает использовать текущее время).
static function MakeRegression(
n: integer := 300;
nFeatures: integer := 10;
nInformative: integer := 5;
noise: real := 0.1;
coefScale: real := 1.0;
bias: real := 0.0;
nonlinearStrength: real := 0.0;
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
/// Генерирует синтетический датасет из двух концентрических окружностей.
/// Используется для демонстрации задач классификации и кластеризации,
/// где граница разделения является нелинейной.
/// Используется для демонстрации задач классификации и кластеризации, в которых граница разделения является нелинейной.
///
/// Датасет состоит из двух классов:
/// внешний круг (класс 0) и внутренний круг (класс 1).
/// При добавлении шума точки отклоняются от идеальной окружности.
/// Датасет состоит из двух классов: внешний круг (класс 0) и внутренний круг (класс 1)
///
/// Параметры позволяют управлять сложностью задачи:
/// • noise — отклонение точек от идеальной окружности
/// • factor — отношение радиусов внутреннего и внешнего круга
/// • classBalance — доля объектов внутреннего круга
/// • flipProb — вероятность случайной инверсии метки
/// • scale — общий масштаб (радиус внешнего круга)
///
/// Полезен для демонстрации:
/// • преимуществ нелинейных моделей (RandomForest, GradientBoosting, kNN)
/// • работы DBSCAN и спектральной кластеризации
/// • ограничений линейных моделей (LogisticRegression, LinearSVM).
///
/// • n — число объектов.
/// • noise — стандартное отклонение гауссовского шума, добавляемого к координатам.
/// • factor — отношение радиуса внутреннего круга к внешнему (0 < factor < 1).
/// • shuffle — перемешивать ли порядок объектов.
/// • seed — значение генератора случайных чисел (-1 означает использовать текущее время)
/// • ограничений линейных моделей (LogisticRegression, LinearSVM)
/// • преимуществ нелинейных моделей (DecisionTree, RandomForest, kNN)
/// • методов кластеризации (DBSCAN, Spectral Clustering)
static function MakeCircles(
n: integer := 300;
noise: real := 0.05;
factor: real := 0.5;
classBalance: real := 0.5;
flipProb: real := 0.0;
scale: real := 1.0;
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
@ -161,6 +182,31 @@ type
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
/// Генерирует синтетический датасет для задачи классификации.
/// Формирует линейно (или почти линейно) разделимые классы с контролируемым шумом.
///
/// • n — число объектов.
/// • nFeatures — общее число признаков.
/// • nInformative — число информативных признаков.
/// • nRedundant — число линейно зависимых признаков.
/// • noise — уровень шума в модели.
/// • classSep — расстояние между классами.
/// • flipProb — вероятность случайной смены метки (label noise).
/// • classBalance — доля класса 1 (0..1).
/// • shuffle — перемешивание объектов.
/// • seed — генератор случайных чисел (-1 → авто)
static function MakeClassification(
n: integer := 300;
nFeatures: integer := 10;
nInformative: integer := 5;
nRedundant: integer := 2;
noise: real := 0.1;
classSep: real := 1.0;
flipProb: real := 0.0;
classBalance: real := 0.5;
shuffle: boolean := true;
seed: integer := -1): (Matrix, Vector);
/// Загружает датасет по имени.
///
/// Датасет ищется в каталоге:
@ -260,6 +306,12 @@ const
'ValueCounts доступны только для задач классификации!!ValueCounts are only available for classification datasets';
ER_DATASET_TARGET_MISSING =
'Target обязателен для задач обучения с учителем!!Target is required for supervised learning datasets';
ER_PARAM_LE =
'Параметр {0} должен быть <= допустимого максимума!!Parameter {0} must be <= the allowed maximum value';
ER_PARAM_RANGE_01 =
'Параметр {0} должен быть в диапазоне (0, 1)!!Parameter {0} must be in range (0, 1)';
ER_PARAM_LT =
'Параметр {0} должен быть меньше допустимого значения!!Parameter {0} must be less than allowed value';
C_DATASET = 'Датасет: {0}!!Dataset: {0}';
C_DESCRIPTION = 'Описание:!!Description:';
@ -480,9 +532,10 @@ end;
//-----------------------------
static function Datasets.MakeBlobs(
n: integer; centers: integer;
clusterStd: real; nFeatures: integer;
shuffle: boolean; seed: integer): (Matrix, Vector);
n, centers, nFeatures: integer;
clusterStd, clusterStdVar, centerBox, classBalance: real;
noisePoints: integer; shuffle: boolean;
seed: integer): (Matrix, Vector);
begin
if n <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'n');
@ -490,43 +543,115 @@ begin
if centers <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'centers');
if clusterStd <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'clusterStd');
if nFeatures <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'nFeatures');
if clusterStd <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'clusterStd');
if clusterStdVar < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'clusterStdVar');
if centerBox <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'centerBox');
if (classBalance <= 0) or (classBalance > 1) then
ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance');
if noisePoints < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noisePoints');
if noisePoints >= n then
ArgumentOutOfRangeError(ER_PARAM_LT, 'noisePoints');
var actualSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var X := new Matrix(n, nFeatures);
var y := new Vector(n);
// --- генерируем центры кластеров
// --- центры
var centersM := new Matrix(centers, nFeatures);
for var c := 0 to centers - 1 do
for var j := 0 to nFeatures - 1 do
centersM[c, j] := rnd.NextDouble * 20 - 10;
centersM[c, j] := (rnd.NextDouble * 2 - 1) * centerBox;
// --- индексы строк
// --- std по кластерам
var stds := new real[centers];
for var c := 0 to centers - 1 do
stds[c] := clusterStd * (1 + clusterStdVar * (2 * rnd.NextDouble - 1));
// --- вероятности кластеров
var probs := new real[centers];
if classBalance = 1 then
begin
for var c := 0 to centers - 1 do
probs[c] := 1.0 / centers;
end
else
begin
var sum := 0.0;
for var c := 0 to centers - 1 do
begin
var p := Power(rnd.NextDouble, 1 / classBalance);
probs[c] := p;
sum += p;
end;
for var c := 0 to centers - 1 do
probs[c] /= sum;
end;
// --- CDF
var cdf := new real[centers];
cdf[0] := probs[0];
for var c := 1 to centers - 1 do
cdf[c] := cdf[c-1] + probs[c];
// --- индексы
var idx := Arr(0..n - 1);
if shuffle then
PABCSystem.Shuffle(idx, rnd);
// --- генерация точек
for var i := 0 to n - 1 do
var mainCount := n - noisePoints;
// --- генерация
for var i := 0 to mainCount - 1 do
begin
var row := idx[i];
var c := rnd.Next(centers);
// --- выбор кластера (inline вместо функции)
var r := rnd.NextDouble;
var c := 0;
while (c < centers - 1) and (r > cdf[c]) do
c += 1;
y[row] := c;
var std := stds[c];
for var j := 0 to nFeatures - 1 do
X[row, j] := centersM[c, j] + clusterStd * Normal(rnd);
X[row, j] := centersM[c, j] + std * Normal(rnd);
end;
// --- шум
for var i := mainCount to n - 1 do
begin
var row := idx[i];
y[row] := -1;
for var j := 0 to nFeatures - 1 do
X[row, j] := (rnd.NextDouble * 2 - 1) * centerBox;
end;
Result := (X, y);
@ -601,7 +726,11 @@ end;
static function Datasets.MakeRegression(
n: integer;
nFeatures: integer;
nInformative: integer;
noise: real;
coefScale: real;
bias: real;
nonlinearStrength: real;
shuffle: boolean;
seed: integer): (Matrix, Vector);
begin
@ -611,22 +740,36 @@ begin
if nFeatures <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'nFeatures');
if nInformative < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'nInformative');
if nInformative > nFeatures then
nInformative := nFeatures;
if noise < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noise');
if coefScale <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'coefScale');
var actualSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var X := new Matrix(n, nFeatures);
var y := new Vector(n);
// --- истинные коэффициенты β
// --- коэффициенты
var beta := new Vector(nFeatures);
for var j := 0 to nFeatures - 1 do
beta[j] := Normal(rnd);
if j < nInformative then
beta[j] := coefScale * Normal(rnd)
else
beta[j] := 0.0;
var idx := Arr(0..n - 1);
if shuffle then
@ -637,7 +780,7 @@ begin
begin
var row := idx[i];
var s := 0.0;
var s := bias;
for var j := 0 to nFeatures - 1 do
begin
@ -646,14 +789,25 @@ begin
s += xx * beta[j];
end;
// --- добавляем нелинейность (по первому признаку)
if nonlinearStrength <> 0 then
begin
var x0 := X[row, 0];
s += nonlinearStrength * x0 * x0;
end;
// --- шум
y[row] := s + noise * Normal(rnd);
end;
Result := (X, y);
end;
static function Datasets.MakeCircles(n: integer; noise: real;
factor: real; shuffle: boolean; seed: integer): (Matrix, Vector);
static function Datasets.MakeCircles(
n: integer;
noise, factor, classBalance, flipProb, scale: real;
shuffle: boolean;
seed: integer): (Matrix, Vector);
begin
if n <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'n');
@ -662,57 +816,62 @@ begin
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noise');
if (factor <= 0) or (factor >= 1) then
ArgumentOutOfRangeError(ER_PARAM_BETWEEN_01, 'factor');
ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'factor');
if (classBalance <= 0) or (classBalance >= 1) then
ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance');
if flipProb < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'flipProb');
if scale <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'scale');
var actualSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var X := new Matrix(n, 2);
var y := new Vector(n);
var idx := Arr(0..n - 1);
var idx := Arr(0..n-1);
if shuffle then
PABCSystem.Shuffle(idx, rnd);
var half := n div 2;
// --- внешний круг
for var i := 0 to half - 1 do
for var i := 0 to n - 1 do
begin
var row := idx[i];
var t := 2*Pi*i/half + 0.1*Normal(rnd);
y[row] := 0;
// --- класс
var label1 := Ord(rnd.NextDouble < classBalance);
X[row, 0] := Cos(t);
X[row, 1] := Sin(t);
// --- радиус
var r :=
if label1 = 1 then
scale * factor
else
scale;
if noise > 0 then
begin
X[row, 0] += noise * Normal(rnd);
X[row, 1] += noise * Normal(rnd);
end;
end;
// --- угол
var angle := 2 * Pi * rnd.NextDouble;
// --- внутренний круг
for var i := half to n - 1 do
begin
var row := idx[i];
var t := 2*Pi*(i-half)/half + 0.1*Normal(rnd);
var xx := r * Cos(angle);
var yv := r * Sin(angle);
y[row] := 1;
// --- шум
xx += noise * Normal(rnd);
yv += noise * Normal(rnd);
X[row, 0] := factor * Cos(t);
X[row, 1] := factor * Sin(t);
X[row, 0] := xx;
X[row, 1] := yv;
if noise > 0 then
begin
X[row, 0] += noise * Normal(rnd);
X[row, 1] += noise * Normal(rnd);
end;
// --- flip labels
if rnd.NextDouble < flipProb then
label1 := 1 - label1;
y[row] := label1;
end;
Result := (X, y);
@ -775,6 +934,107 @@ begin
Result := (X, y);
end;
static function Datasets.MakeClassification(
n, nFeatures, nInformative, nRedundant: integer;
noise, classSep, flipProb, classBalance: real;
shuffle: boolean;
seed: integer): (Matrix, Vector);
begin
if n <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'n');
if nFeatures <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'nFeatures');
if nInformative <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'nInformative');
if nInformative > nFeatures then
ArgumentOutOfRangeError(ER_PARAM_LE, 'nInformative');
if nRedundant < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'nRedundant');
if nInformative + nRedundant > nFeatures then
ArgumentOutOfRangeError(ER_PARAM_LE, 'nRedundant');
if noise < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noise');
if classSep <= 0 then
ArgumentOutOfRangeError(ER_PARAM_GT_ZERO, 'classSep');
if (classBalance <= 0) or (classBalance >= 1) then
ArgumentOutOfRangeError(ER_PARAM_RANGE_01, 'classBalance');
if flipProb < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'flipProb');
var actualSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var X := new Matrix(n, nFeatures);
var y := new Vector(n);
// --- направление разделения классов
var center := new Vector(nInformative);
for var j := 0 to nInformative - 1 do
center[j] := Normal(rnd);
// --- нормализуем направление, чтобы classSep имел предсказуемый смысл
var norm := 0.0;
for var j := 0 to nInformative - 1 do
norm += center[j] * center[j];
norm := Sqrt(norm);
if norm > 0 then
for var j := 0 to nInformative - 1 do
center[j] := center[j] / norm;
// --- индексы
var idx := Arr(0..n-1);
if shuffle then
PABCSystem.Shuffle(idx, rnd);
for var i := 0 to n - 1 do
begin
var row := idx[i];
// --- сначала выбираем класс
var label1 := Ord(rnd.NextDouble < classBalance);
var sign := if label1 = 1 then 1.0 else -1.0;
// --- информативные признаки:
// два класса имеют разные центры вдоль направления center
for var j := 0 to nInformative - 1 do
X[row, j] := Normal(rnd) + sign * classSep * center[j] + noise * Normal(rnd);
// --- редундантные признаки: линейные комбинации информативных
for var j := 0 to nRedundant - 1 do
begin
var s := 0.0;
for var k := 0 to nInformative - 1 do
s += Normal(rnd) * X[row, k];
X[row, nInformative + j] := s / nInformative;
end;
// --- шумовые признаки
for var j := nInformative + nRedundant to nFeatures - 1 do
X[row, j] := Normal(rnd);
// --- шум в метках
if rnd.NextDouble < flipProb then
label1 := 1 - label1;
y[row] := label1;
end;
Result := (X, y);
end;
function ParseTask(s: string): TaskType;
begin
case s.ToLower of

View file

@ -253,9 +253,18 @@ type
/// Возвращает массив меток классов в порядке столбцов PredictProba.
function GetClasses: array of real;
/// Возвращает вектор предсказанных классов.
/// Возвращает предсказанные классы для объектов из X.
/// Результат — вектор значений, где каждый элемент соответствует классу объекта.
/// Порядок элементов соответствует строкам матрицы X.
/// Требует предварительного вызова Fit.
function Predict(X: Matrix): Vector;
/// Возвращает предсказанные метки классов для объектов из X.
/// Каждый элемент результата — индекс класса (целое число).
/// Порядок элементов соответствует строкам матрицы X.
/// Требует предварительного вызова Fit.
function PredictLabels(X: Matrix): array of integer;
/// Показывает, была ли модель обучена.
/// Если false — вызов Predict или PredictProba приведет к ошибке.
property IsFitted: boolean read fFitted;
@ -469,6 +478,12 @@ type
/// Для каждого объекта возвращается класс, соответствующий листу дерева.
function Predict(X: Matrix): Vector; override;
/// Возвращает предсказанные метки классов для объектов из X.
/// Каждый элемент результата — индекс класса (целое число).
/// Порядок элементов соответствует строкам матрицы X.
/// Требует предварительного вызова Fit.
function PredictLabels(X: Matrix): array of integer;
/// Создает глубокую копию дерева классификации.
/// Копируется структура узлов, параметры и обученное состояние.
function Clone: IModel; override;
@ -725,6 +740,12 @@ type
/// Итоговый класс определяется большинством голосов или максимальной суммарной вероятностью.
function Predict(X: Matrix): Vector; override;
/// Возвращает предсказанные метки классов для объектов из X.
/// Каждый элемент результата — индекс класса (целое число).
/// Порядок элементов соответствует строкам матрицы X.
/// Требует предварительного вызова Fit.
function PredictLabels(X: Matrix): array of integer;
function PredictProba(X: Matrix): Matrix;
/// Создает глубокую копию случайного леса классификации.
@ -1016,6 +1037,8 @@ type
/// Возвращает исходные значения классов, а не внутренние индексы.
function Predict(X: Matrix): Vector;
function PredictLabels(X: Matrix): array of integer;
/// Возвращает вероятности принадлежности к каждому классу.
/// Размер результата: [nSamples x nClasses].
/// Вероятности получаются через softmax.
@ -1156,6 +1179,8 @@ type
/// Возвращает вектор предсказанных меток
function Predict(X: Matrix): Vector; override;
function PredictLabels(X: Matrix): array of integer;
/// Возвращает матрицу вероятностей размера (nSamples × nClasses).
/// Столбцы соответствуют классам в порядке, возвращаемом GetClasses()
function PredictProba(X: Matrix): Matrix;
@ -2699,6 +2724,16 @@ begin
end;
end;
function LogisticRegression.PredictLabels(X: Matrix): array of integer;
begin
var v := Predict(X);
Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]);
end;
function LogisticRegression.ToString: string;
begin
Result :=
@ -3513,6 +3548,16 @@ begin
end;
end;
function DecisionTreeClassifier.PredictLabels(X: Matrix): array of integer;
begin
var v := Predict(X);
Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]);
end;
function DecisionTreeClassifier.Clone: IModel;
begin
var m := new DecisionTreeClassifier(
@ -4311,6 +4356,16 @@ begin
Result := resultVec;
end;
function RandomForestClassifier.PredictLabels(X: Matrix): array of integer;
begin
var v := Predict(X);
Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]);
end;
function RandomForestClassifier.PredictProba(X: Matrix): Matrix;
begin
if not fFitted then
@ -6194,6 +6249,16 @@ begin
end;
end;
function GradientBoostingClassifier.PredictLabels(X: Matrix): array of integer;
begin
var v := Predict(X);
Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]);
end;
//-----------------------------
// KNNBase
//-----------------------------
@ -6512,6 +6577,16 @@ begin
end;
end;
function KNNClassifier.PredictLabels(X: Matrix): array of integer;
begin
var v := Predict(X);
Result := new integer[v.Length];
for var i := 0 to v.Length - 1 do
Result[i] := integer(v[i]);
end;
function KNNClassifier.PredictProba(X: Matrix): Matrix;
begin
if not fFitted then

View file

@ -15,7 +15,7 @@ uses LinearAlgebraML;
type
/// Метрики оценки качества моделей машинного обучения
/// для задач регрессии и классификации.
/// для задач регрессии, классификации и кластеризации
Metrics = static class
public
/// Среднеквадратичная ошибка (MSE).
@ -49,7 +49,7 @@ type
/// Сильно штрафует уверенные, но ошибочные прогнозы.
/// Используется для моделей, возвращающих вероятности
/// (например, LogisticRegression).
static function LogLoss(yTrue, yProb: Vector): real;
static function LogLoss(yTrue, yPred: Vector): real;
// ---- ROC / AUC ----
@ -57,13 +57,13 @@ type
/// Возвращает кортеж из двух векторов:
/// FPR (доля ложных положительных)
/// и TPR (доля истинно положительных).
static function ROC(yTrue, yProb: Vector): (Vector, Vector);
static function ROC(yTrue, yPred: Vector): (Vector, Vector);
/// Площадь под ROC-кривой (AUC).
/// Значение от 0 до 1.
/// 0.5 — случайная модель,
/// 1 — идеальная модель.
static function AUC(yTrue, yProb: Vector): real;
static function AUC(yTrue, yPred: Vector): real;
/// Доля правильных предсказаний.
/// Показывает, какая часть объектов классифицирована верно.
@ -71,6 +71,18 @@ type
/// Пример: Accuracy = 0.9 означает 90% правильных ответов.
static function Accuracy(yTrue, yPred: Vector): real;
/// Доля правильных предсказаний.
/// Показывает, какая часть объектов классифицирована верно.
/// Удобна, когда классы примерно сбалансированы.
/// Пример: Accuracy = 0.9 означает 90% правильных ответов.
static function Accuracy(yTrue, yPred: array of integer): real;
/// Доля правильных предсказаний.
/// Показывает, какая часть объектов классифицирована верно.
/// Удобна, когда классы примерно сбалансированы.
/// Пример: Accuracy = 0.9 означает 90% правильных ответов.
static function Accuracy(yTrue: Vector; yPred: array of integer): real;
/// Точность (Precision) для положительного класса (1).
/// Среди всех объектов, которые модель предсказала как 1,
/// показывает долю действительно принадлежащих этому классу.
@ -203,6 +215,166 @@ type
function BalancedAccuracy: real;
end;
/// Метрики для оценки качества классификационных моделей
/// Используются когда целевая переменная является категорией (классом)
/// (например, определение вида цветка, спам/не спам, диагноз пациента)
ClassificationMetrics = static class
public
/// Доля правильных предсказаний (Accuracy)
/// Accuracy = (TP + TN) / (TP + TN + FP + FN)
/// Показывает долю верно классифицированных объектов
/// Удобна когда классы примерно сбалансированы
static function Accuracy(yTrue, yPred: Vector): real := Metrics.Accuracy(yTrue, yPred);
/// Доля правильных предсказаний (Accuracy)
/// Accuracy = (TP + TN) / (TP + TN + FP + FN)
/// Показывает долю верно классифицированных объектов
/// Удобна когда классы примерно сбалансированы
static function Accuracy(yTrue, yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred);
/// Доля правильных предсказаний (Accuracy)
/// Accuracy = (TP + TN) / (TP + TN + FP + FN)
/// Показывает долю верно классифицированных объектов
/// Удобна когда классы примерно сбалансированы
static function Accuracy(yTrue: Vector; yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred);
/// Точность (Precision) для положительного класса (1)
/// Precision = TP / (TP + FP)
/// Среди всех предсказанных положительных объектов показывает долю истинно положительных
/// Важна когда критичны ложные положительные результаты ("ложные тревоги")
static function Precision(yTrue, yPred: Vector): real := Metrics.Precision(yTrue, yPred);
/// Полнота (Recall) для положительного класса (1)
/// Recall = TP / (TP + FN)
/// Среди всех истинно положительных объектов показывает долю правильно найденных
/// Важна когда нежелательны пропуски положительных случаев (например, заболеваний)
static function Recall(yTrue, yPred: Vector): real := Metrics.Recall(yTrue, yPred);
/// F1-мера
/// F1 = 2 * (Precision * Recall) / (Precision + Recall)
/// Гармоническое среднее точности и полноты
/// Высока только когда оба показателя высоки
/// Особенно полезна при несбалансированных классах
static function F1(yTrue, yPred: Vector): real := Metrics.F1(yTrue, yPred);
/// Специфичность (Specificity / True Negative Rate)
/// Specificity = TN / (TN + FP)
/// Доля правильно предсказанных отрицательных объектов
/// Показывает способность модели корректно распознавать отрицательный класс
static function Specificity(yTrue, yPred: Vector): real := Metrics.Specificity(yTrue, yPred);
/// Сбалансированная точность (Balanced Accuracy)
/// Balanced Accuracy = (Recall + Specificity) / 2
/// Среднее арифметическое полноты и специфичности
/// Устойчива к дисбалансу классов
static function BalancedAccuracy(yTrue, yPred: Vector): real := Metrics.BalancedAccuracy(yTrue, yPred);
/// Логарифмическая функция потерь (LogLoss)
/// LogLoss = - (1/n) * Σ [yTrue * log(yProb) + (1 - yTrue) * log(1 - yProb)]
/// Оценивает качество вероятностных предсказаний
/// Сильно штрафует уверенные, но ошибочные прогнозы
/// Чем меньше значение, тем лучше модель
static function LogLoss(yTrue, yPred: Vector): real := Metrics.LogLoss(yTrue, yPred);
/// Построение ROC-кривой
/// Возвращает кортеж из двух векторов: FPR (ложноположительные) и TPR (истинноположительные)
/// Используется для визуализации качества бинарной классификации
static function ROC(yTrue, yPred: Vector): (Vector, Vector) := Metrics.ROC(yTrue, yPred);
/// Площадь под ROC-кривой (AUC)
/// Значение от 0 до 1:
/// 1.0 — идеальная модель
/// 0.5 — случайная модель
/// <0.5 — модель хуже случайной
static function AUC(yTrue, yPred: Vector): real := Metrics.AUC(yTrue, yPred);
end;
/// Метрики для оценки качества регрессионных моделей
/// Используются когда целевая переменная является непрерывным числом
/// (например, предсказание цены дома, температуры, возраста)
RegressionMetrics = static class
public
/// Среднеквадратичная ошибка (MSE)
/// MSE = (1/n) * Σ (yTrue - yPred)^2
/// Чувствительна к большим отклонениям и выбросам
/// Чем меньше значение, тем лучше модель
static function MSE(yTrue, yPred: Vector): real := Metrics.MSE(yTrue, yPred);
/// Корень из среднеквадратичной ошибки (RMSE)
/// RMSE = √MSE
/// Измеряется в тех же единицах, что и целевая переменная
/// Чем меньше значение, тем лучше модель
static function RMSE(yTrue, yPred: Vector): real := Metrics.RMSE(yTrue, yPred);
/// Средняя абсолютная ошибка (MAE)
/// MAE = (1/n) * Σ |yTrue - yPred|
/// Менее чувствительна к выбросам, чем MSE
/// Чем меньше значение, тем лучше модель
static function MAE(yTrue, yPred: Vector): real := Metrics.MAE(yTrue, yPred);
/// Коэффициент детерминации (R²)
/// R² = 1 - SS_res / SS_tot
/// Показывает долю дисперсии, объяснённую моделью
/// Значения:
/// 1.0 — идеальная модель
/// 0.0 — модель не лучше среднего
/// <0 — модель хуже простого среднего
static function R2(yTrue, yPred: Vector): real := Metrics.R2(yTrue, yPred);
end;
/// Метрики для оценки качества кластеризации
/// Используются когда нужно оценить, насколько хорошо объекты сгруппированы в кластеры
/// Делятся на:
/// - внутренние (без эталонных меток) — оценивают компактность и разделимость
/// - внешние (с эталонными метками) — сравнивают с правильным разбиением
ClusteringMetrics = static class
public
// ========== ВНУТРЕННИЕ МЕТРИКИ (без эталонных меток) ==========
/// Вычисляет силуэт (Silhouette) для каждой точки
/// Для каждой строки матрицы X возвращает значение s(i) в диапазоне [-1, 1]
/// s(i) ≈ 1 — точка хорошо сгруппирована в своём кластере
/// s(i) ≈ 0 — точка находится на границе между кластерами
/// s(i) < 0 — точка, вероятно, попала не в свой кластер
/// Требует как минимум 2 различных кластера
/// Сложность O(n²) — для умеренных размеров данных
static function SilhouetteSamples(X: Matrix; labels: Vector): Vector := Metrics.SilhouetteSamples(x, labels);
/// Вычисляет средний силуэт (Silhouette Score)
/// Возвращает среднее значение Silhouette по всем точкам
/// Чем ближе к 1 — тем лучше разделены кластеры
/// Значение около 0 означает пересечение кластеров
/// Отрицательное значение указывает на возможную ошибку кластеризации
static function SilhouetteScore(X: Matrix; labels: Vector): real := Metrics.SilhouetteScore(x, labels);
/// Индекс Калински–Харабаса (Calinski–Harabasz Index)
/// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии
/// Чем больше значение — тем лучше разделены кластеры
/// Требует как минимум 2 кластера и n > k
/// Сложность O(n·p)
static function CalinskiHarabaszScore(X: Matrix; labels: Vector): real := Metrics.CalinskiHarabaszScore(x, labels);
/// Индекс Дэвиса–Булдина (Davies–Bouldin Index)
/// Чем меньше значение — тем лучше разделены кластеры
/// Минимальное значение 0 (идеальное разделение)
/// Требует минимум 2 кластера
/// Сложность O(n·p + k²·p)
static function DaviesBouldinScore(X: Matrix; labels: Vector): real := Metrics.DaviesBouldinScore(x, labels);
// ========== ВНЕШНИЕ МЕТРИКИ (с эталонными метками) ==========
/// Скорректированный индекс Рэнда (Adjusted Rand Index, ARI)
/// Сравнивает предсказанные кластеры с истинными метками
/// Основан на анализе всех пар объектов:
/// проверяет, совпадает ли решение алгоритма и истинная разметка
/// в вопросе "находятся ли две точки в одном кластере"
/// Значения:
/// 1.0 — идеальное совпадение кластеризации и истинных меток
/// 0.0 — случайное разбиение
/// <0 — хуже случайного
static function AdjustedRandIndex(yTrue, yPred: Vector): real := Metrics.AdjustedRandIndex(yTrue, yPred);
end;
implementation
uses MLExceptions;
@ -362,18 +534,18 @@ begin
Result := 1 - ssRes / ssTot;
end;
static function Metrics.LogLoss(yTrue, yProb: Vector): real;
static function Metrics.LogLoss(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
var n := yTrue.Length;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'LogLoss');
@ -384,13 +556,13 @@ begin
for var i := 0 to n - 1 do
begin
var yt := yTrue[i];
var p := yProb[i];
var p := yPred[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(p) or double.IsInfinity(p) then
ArgumentError(ER_INVALID_VALUE, 'yProb', i);
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
if (yt <> 0.0) and (yt <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yt);
@ -404,18 +576,18 @@ begin
Result := -s / n;
end;
static function Metrics.ROC(yTrue, yProb: Vector): (Vector, Vector);
static function Metrics.ROC(yTrue, yPred: Vector): (Vector, Vector);
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'ROC');
@ -426,13 +598,13 @@ begin
for var i := 0 to n - 1 do
begin
var yt := yTrue[i];
var p := yProb[i];
var p := yPred[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(p) or double.IsInfinity(p) then
ArgumentError(ER_INVALID_VALUE, 'yProb', i);
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
if (yt <> 0.0) and (yt <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yt);
@ -448,7 +620,7 @@ begin
var idx :=
(0..n-1)
.OrderByDescending(i -> yProb[i])
.OrderByDescending(i -> yPred[i])
.ToArray;
var tp := 0;
@ -477,23 +649,23 @@ begin
new Vector(tprList.ToArray));
end;
static function Metrics.AUC(yTrue, yProb: Vector): real;
static function Metrics.AUC(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'AUC');
var (fpr, tpr) := ROC(yTrue, yProb);
var (fpr, tpr) := ROC(yTrue, yPred);
if fpr.Length <> tpr.Length then
DimensionError(ER_DIM_MISMATCH, fpr.Length, tpr.Length);
@ -546,6 +718,55 @@ begin
Result := correct / n;
end;
static function Metrics.Accuracy(yTrue, yPred: array of integer): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'Accuracy');
var correct := 0;
for var i := 0 to n - 1 do
if yTrue[i] = yPred[i] then
correct += 1;
Result := correct / n;
end;
static function Metrics.Accuracy(yTrue: Vector; yPred: array of integer): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
var data := yTrue.Data;
var n := data.Length;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, n, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'Accuracy');
var correct := 0;
for var i := 0 to n - 1 do
if integer(data[i]) = yPred[i] then
correct += 1;
Result := correct / n;
end;
static function Metrics.Precision(yTrue, yPred: Vector): real;
begin
Result := ConfusionMatrix.Create(yTrue, yPred).Precision;

View file

@ -36,21 +36,33 @@ const DefaultColor = default(ColorWPF);
type
MarkerType = (Circle, Box, Triangle, Diamond, Cross);
Palettes = static class
public
const &Default = 'default';
const Dark = 'dark';
const Pastel = 'pastel';
const Bright = 'bright';
const Muted = 'muted';
end;
Palette = class
public
Colors: array of Color;
constructor Create(params c: array of Color);
end;
PaletteWPF = Palette;
Palettes = static class
private
static paletteDict: Dictionary<string, Palette>;
static constructor;
static procedure InitPalettes;
public
static function &Default: Palette;
static function Pastel: Palette;
static function Dark: Palette;
static function Bright: Palette;
static function Muted: Palette;
static procedure Register(name: string; p: Palette);
static function Get(name: string): Palette;
end;
Figure = class;
Cell = class
@ -65,6 +77,7 @@ type
function NextColor: Color;
procedure EnsureChart;
procedure SetTitle(s: string);
public
constructor Create(g: GridWPF; r,c: integer);
@ -87,12 +100,16 @@ type
size: real := 6; marker: MarkerType := MarkerType.Circle);
procedure Hist(x: Vector; bins: integer := 0;
color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil);
//procedure Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette := nil);
procedure Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette := nil);
procedure Heatmap(m: Matrix);
procedure Text(s: string; x: real := 0.5; y: real := 0.5);
procedure SetPalette(name: string);
procedure Title(s: string);
procedure SetPalette(p: PaletteWPF);
procedure XLabel(s: string);
procedure YLabel(s: string);
@ -101,6 +118,8 @@ type
procedure YLim(ymin,ymax: real);
procedure Clear;
property Title: string write SetTitle;
end;
Figure = class
@ -133,6 +152,12 @@ type
static procedure DrawHist(chart: ChartWPF; x: array of real;
bins: integer; color: ColorWPF; alpha: real; legend: string);
static procedure DrawSurface(chart: ChartWPF; labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
static function MakeGrid(xmin, xmax, ymin, ymax: real; nx, ny: integer): Matrix;
static procedure SetTitle(s: string);
public
static procedure AddSeries(chart: ChartWPF; series: UIElement);
@ -152,8 +177,11 @@ type
static procedure Heatmap(m: array[,] of real);
static procedure Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette := nil);
static procedure Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette := nil);
// с матрицами - векторами
// --- Vector overloads
static procedure LineGraph(x, y: Vector;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil)
@ -180,14 +208,13 @@ type
static function Grid(rows,cols: integer): Figure;
static procedure SetPalette(name: string);
static procedure SetPalette(p: PaletteWPF);
static procedure EnsureAxes(chart: ChartWPF);
static procedure Limits(xmin,xmax,ymin,ymax: real);
static procedure XLim(xmin,xmax: real);
static procedure YLim(ymin,ymax: real);
static procedure Title(s: string);
static procedure XLabel(s: string);
static procedure YLabel(s: string);
static procedure SetLabels(title: string := ''; xlabel: string := ''; ylabel: string := '');
@ -195,6 +222,8 @@ type
static procedure Clear;
static procedure Save(filename: string);
static property Title: string write SetTitle;
end;
HistogramPlot = class(PlotWPF)
@ -218,6 +247,14 @@ type
property MaxCount: integer read fMaxCount;
end;
SurfacePlot = class(PlotWPF)
private
fRects: List<Polygon> := new List<Polygon>;
public
procedure SetData(labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
end;
implementation
var
@ -232,50 +269,31 @@ var
currentPalette: Palette;
rootPaletteIndex: integer := 0;
function CreateMarker(t: MarkerType): InteractiveDataDisplay.WPF.ColorSizeMarker;
static function Palettes.&Default: Palette := paletteDict['default'];
static function Palettes.Pastel: Palette := paletteDict['pastel'];
static function Palettes.Dark: Palette := paletteDict['dark'];
static function Palettes.Bright: Palette := paletteDict['bright'];
static function Palettes.Muted: Palette := paletteDict['muted'];
static constructor Palettes.Create;
begin
case t of
MarkerType.Circle: Result := new CircleMarker;
MarkerType.Box: Result := new BoxMarker;
MarkerType.Triangle: Result := new TriangleMarker;
MarkerType.Diamond: Result := new DiamondMarker;
MarkerType.Cross: Result := new CrossMarker;
end;
InitPalettes
end;
function NextRootColor: ColorWPF;
static procedure Palettes.Register(name: string; p: Palette);
begin
var c := currentPalette.Colors[
rootPaletteIndex mod currentPalette.Colors.Length
];
rootPaletteIndex += 1;
Result := c;
paletteDict[name] := p;
end;
function MakeHistogram(data: array of real; bins: integer): (array of real, array of real);
static function Palettes.Get(name: string): Palette;
begin
var xmin := data.Min;
var xmax := data.Max;
var h := (xmax-xmin)/bins;
var counts := new real[bins];
foreach var v in data do
begin
var k := integer((v-xmin)/h);
if k=bins then k := bins-1;
counts[k] += 1;
end;
var xs := ArrGen(bins, i -> xmin + (i+0.5)*h);
Result := (xs,counts);
Result := paletteDict[name];
end;
procedure InitPalettes;
static procedure Palettes.InitPalettes;
begin
if paletteDict <> nil then exit;
paletteDict := new Dictionary<string,Palette>;
paletteDict['default'] := new Palette(
@ -334,6 +352,50 @@ begin
currentPalette := paletteDict['default'];
end;
function CreateMarker(t: MarkerType): InteractiveDataDisplay.WPF.ColorSizeMarker;
begin
case t of
MarkerType.Circle: Result := new CircleMarker;
MarkerType.Box: Result := new BoxMarker;
MarkerType.Triangle: Result := new TriangleMarker;
MarkerType.Diamond: Result := new DiamondMarker;
MarkerType.Cross: Result := new CrossMarker;
end;
end;
function NextRootColor: ColorWPF;
begin
var c := currentPalette.Colors[
rootPaletteIndex mod currentPalette.Colors.Length
];
rootPaletteIndex += 1;
Result := c;
end;
function MakeHistogram(data: array of real; bins: integer): (array of real, array of real);
begin
var xmin := data.Min;
var xmax := data.Max;
var h := (xmax-xmin)/bins;
var counts := new real[bins];
foreach var v in data do
begin
var k := integer((v-xmin)/h);
if k=bins then k := bins-1;
counts[k] += 1;
end;
var xs := ArrGen(bins, i -> xmin + (i+0.5)*h);
Result := (xs,counts);
end;
procedure InitUI;
begin
uiThread := new Thread(() ->
@ -363,7 +425,7 @@ begin
uiDispatcher := Dispatcher.CurrentDispatcher;
InitPalettes;
Palettes.InitPalettes;
win.Show;
@ -401,16 +463,15 @@ begin
Result := c;
end;
procedure Cell.SetPalette(name: string);
procedure Cell.SetPalette(p: PaletteWPF);
begin
if paletteDict.ContainsKey(name) then
begin
palette := paletteDict[name];
paletteIndex := 0;
end;
if p = nil then exit;
palette := p;
paletteIndex := 0;
end;
procedure Cell.Title(s: string);
procedure Cell.SetTitle(s: string);
begin
Plot.RunUI(() ->
begin
@ -542,7 +603,7 @@ begin
var classes := labels.Distinct.ToArray;
&Array.Sort(classes);
var pal := CurrentPalette;
var pal := palette;
foreach var c in classes do
begin
@ -671,7 +732,7 @@ end;
var gridMode := false;
static procedure Plot.Title(s: string);
static procedure Plot.SetTitle(s: string);
begin
RunUI(() ->
begin
@ -970,12 +1031,13 @@ begin
Result := fig;
end;
static procedure Plot.SetPalette(name: string);
static procedure Plot.SetPalette(p: PaletteWPF);
begin
RunUI(() ->
begin
if name in paletteDict then
currentPalette := paletteDict[name];
if p = nil then exit;
currentPalette := p;
end);
end;
@ -1235,6 +1297,138 @@ begin
fMaxCount := counts.Max;
end;
procedure SurfacePlot.SetData(labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
begin
Children.Clear;
var dx := (xmax - xmin) / nx;
var dy := (ymax - ymin) / ny;
var k := 0;
for var iy := 0 to ny - 1 do
for var ix := 0 to nx - 1 do
begin
var lab := labels[k];
k += 1;
var clr := pal.Colors[Abs(lab) mod pal.Colors.Length];
var brush := new SolidColorBrush(clr);
var x0 := xmin + ix * dx;
var y0 := ymin + iy * dy;
var eps := dx * 0.01;
var x1 := x0 + dx + eps;
var y1 := y0 + dy + eps;
var poly := new Polygon;
var pts := new PointCollection;
pts.Add(new Point(x0, y0));
pts.Add(new Point(x0, y1));
pts.Add(new Point(x1, y1));
pts.Add(new Point(x1, y0));
PlotWPF.SetPoints(poly, pts);
poly.Fill := brush;
poly.StrokeThickness := 0;
Children.Add(poly);
end;
end;
static procedure Plot.DrawSurface(chart: ChartWPF;
labels: array of integer; nx, ny: integer;
xmin, xmax, ymin, ymax: real;
pal: Palette);
begin
EnsureAxes(chart);
var s := new SurfacePlot;
s.SetData(labels, nx, ny, xmin, xmax, ymin, ymax, pal);
AddSeries(chart, s);
end;
static function Plot.MakeGrid(xmin, xmax, ymin, ymax: real; nx, ny: integer): Matrix;
begin
Result := new Matrix(nx * ny, 2);
var k := 0;
for var iy := 0 to ny - 1 do
begin
var y := ymin + (ymax - ymin) * iy / (ny - 1);
for var ix := 0 to nx - 1 do
begin
var x := xmin + (xmax - xmin) * ix / (nx - 1);
Result[k, 0] := x;
Result[k, 1] := y;
k += 1;
end;
end;
end;
{procedure Cell.Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var usePal := if pal <> nil then pal else palette;
Plot.DrawSurface(chart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;}
procedure Cell.Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var xmin := x1.Min - 0.5;
var xmax := x1.Max + 0.5;
var ymin := x2.Min - 0.5;
var ymax := x2.Max + 0.5;
var G := Plot.MakeGrid(xmin, xmax, ymin, ymax, nx, ny);
var labels := f(G);
var usePal := if pal <> nil then pal else palette;
Plot.DrawSurface(chart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
static procedure Plot.Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette);
begin
RunUI(() ->
begin
var usePal := if pal <> nil then pal else CurrentPalette;
DrawSurface(rootChart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
static procedure Plot.Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette);
begin
RunUI(() ->
begin
var xmin := x1.Min - 0.5;
var xmax := x1.Max + 0.5;
var ymin := x2.Min - 0.5;
var ymax := x2.Max + 0.5;
var G := MakeGrid(xmin, xmax, ymin, ymax, nx, ny);
var labels := f(G);
var usePal := if pal <> nil then pal else CurrentPalette;
DrawSurface(rootChart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
initialization
InitUI;