Настройка проекта
- Создайте новый проект
- Отредактируйте Cargo.toml (файл зависимостей). Добавьте следующие зависимости (версии актуальны на сентябрь 2025; проверьте на crates.io для обновлений):
[package]
name = "ml_infographic"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
crossbeam = "0.8"
image = "0.25"
linfa = "0.7"
linfa-clustering = "0.7"
linfa-logistic = "0.7"
linfa-svm = "0.7"
linfa-kernel = "0.7"
ndarray = { version = "0.15", features = ["rayon"] }
ndarray-rand = "0.14"
polars = { version = "0.43", features = ["lazy", "ndarray"] }
plotters = "0.3"
rand = "0.8"
Генерация данных
- Создайте 1500 точек (по 500 на кластер).
- Используйте ndarray для массивов.
- Перемешайте данные для реализма.
fn main() -> Result<()> {
let mut rng = thread_rng();
let features = 2; // 2D данные
let samples_per_cluster = 500;
let num_clusters = 3;
let total_samples = samples_per_cluster * num_clusters;
let mut data = Array2::<f64>::zeros((total_samples, features));
let mut targets = Array1::<i64>::zeros(total_samples);
for cluster in 0..num_clusters {
let offset = cluster as f64 * 5.0;
let start = cluster * samples_per_cluster;
let end = start + samples_per_cluster;
for i in start..end {
data.row_mut(i).assign(&Array::random_using(features, Uniform::new(offset, offset + 3.0), &mut rng));
targets[i] = cluster as i64;
}
}
// Перемешивание
let mut indices = (0..total_samples).collect::<Vec<_>>();
indices.shuffle(&mut rng);
let data_shuffled = data.select(Axis(0), &indices);
let targets_shuffled = targets.select(Axis(0), &indices);
// Преобразование в DataFrame Polars для обработки
let mut cols = Vec::with_capacity(features + 1);
for f in 0..features {
cols.push(Series::new(format!("feature_{}", f).into(), data_shuffled.column(f).to_vec()));
}
cols.push(Series::new("target".into(), targets_shuffled.to_vec()));
let mut df = DataFrame::new(cols)?;
// Нормализация и добавление взаимодействия (feature engineering)
let lazy_df = df.lazy();
let means = lazy_df.clone().select([col("*").exclude(["target"]).mean()]).collect()?;
let stds = lazy_df.clone().select([col("*").exclude(["target"]).std(1)]).collect()?;
let normalized_df = lazy_df
.select([
((col("feature_0") - lit(means.column("feature_0")?.get(0)?.try_extract::<f64>()?))
/ lit(stds.column("feature_0")?.get(0)?.try_extract::<f64>()?))
.alias("norm_f0"),
((col("feature_1") - lit(means.column("feature_1")?.get(0)?.try_extract::<f64>()?))
/ lit(stds.column("feature_1")?.get(0)?.try_extract::<f64>()?))
.alias("norm_f1"),
(col("feature_0") * col("feature_1")).alias("interaction"),
col("target"),
])
.collect()?;
// Извлечение фич и таргетов
let feature_names = vec!["norm_f0", "norm_f1", "interaction"];
let features_df = normalized_df.select(feature_names.iter().map(|s| col(s)).collect::<Vec<_>>())?;
let features = features_df.to_ndarray::<Float64Type>(IndexOrder::Fortran)?.to_owned();
let targets = normalized_df.column("target")?.i64()?.to_ndarray()?.to_owned();
let dataset = Dataset::new(features.clone(), targets.clone());
// ... (продолжение ниже)
}
Пояснения:
- ndarray::Array2 — многомерный массив для данных.
- polars::DataFrame — для ленивых вычислений (lazy), нормализации (z-score) и добавления взаимодействия (feature_0 * feature_1) для улучшения моделей.
- Dataset из linfa — структура для ML.
Кластеризация с K-Means
- Подбор оптимального k (2-5) по silhouette score.
- Используем L2Dist как метрику расстояния.
let mut best_k = 2;
let mut best_score = f64::MIN;
let mut best_kmeans_model: Option> = None;
for k in 2..=5 {
let model = KMeans::params_with(k, rng.clone(), L2Dist)
.tolerance(1e-3)
.max_n_iterations(200)
.n_runs(20)
.fit(&features)?;
let labels = model.predict(&features);
let score = Dataset::new(features.clone(), labels.mapv(|u| u as i64)).silhouette_score()?;
if score > best_score {
best_score = score;
best_k = k;
best_kmeans_model = Some(model);
}
}
let kmeans_model = best_kmeans_model.unwrap();
let kmeans_labels = kmeans_model.predict(&features);
Пояснения:
- KMeans::params_with — настройка с расстоянием Euclidean (L2).
- fit — обучение.
- silhouette_score — метрика качества кластеризации (ближе к 1 — лучше).
- predict — предсказание меток.
let alphas = vec![0.01, 0.1, 0.5, 1.0, 5.0, 10.0];
let mut best_lr_acc = 0.0;
let mut best_lr_model: Option> = None;
thread::scope(|s| {
let handles = alphas.into_iter().map(|alpha| {
s.spawn(move |_| {
let model = LogisticRegression::default()
.alpha(alpha)
.gradient_tolerance(1e-5)
.max_iterations(1000)
.fit(&dataset)?;
let preds = model.predict(&features);
let acc = preds.iter().zip(targets.iter()).filter(|(&p, &t)| p == *t).count() as f64 / total_samples as f64;
(model, acc)
})
}).collect::>();
for handle in handles {
let (model, acc) = handle.join().unwrap()?;
if acc > best_lr_acc {
best_lr_acc = acc;
best_lr_model = Some(model);
}
}
Ok(())
}).unwrap()?;
let lr_model = best_lr_model.unwrap();
let lr_preds = lr_model.predict(&features);
Пояснения:
- LogisticRegression::default() — базовая модель для многоклассовой классификации.
- alpha — коэффициент L2-регуляризации.
- Параллелизм с crossbeam::thread для ускорения.
- Accuracy — простая метрика (доля правильных предсказаний).
Классификация с SVM
- Grid search по epsilon для Gaussian kernel.
- Используем linfa-kernel для ядер.
let mut best_svm_acc = 0.0;
let mut best_svm_model: Option> = None;
let mut best_epsilon = 0.0;
for epsilon in [0.05, 0.1, 0.2] {
let kernel = Kernel::params().method(KernelMethod::Gaussian(epsilon)).transform(&features);
let model = fit_c(&kernel, &targets, 1.0, 1e-3, true)?;
let preds = model.predict(&kernel);
let acc = preds.iter().zip(targets.iter()).filter(|(&p, &t)| p == t).count() as f64 / total_samples as f64;
if acc > best_svm_acc {
best_svm_acc = acc;
best_svm_model = Some(model);
best_epsilon = epsilon;
}
}
let svm_model = best_svm_model.unwrap();
let train_kernel = Kernel::params().method(KernelMethod::Gaussian(best_epsilon)).transform(&features);
let svm_preds = svm_model.predict(&train_kernel);
Пояснения:
- Kernel::params() — создание ядра (Gaussian для нелинейности).
- fit_c — обучение SVM с параметром C (баланс ошибки и маржи).
- Для предсказаний нужен kernel матрица.
Метрики: Confusion Matrices
let cm_lr = lr_preds.confusion_matrix(&targets)?;
let cm_svm = svm_preds.confusion_matrix(&targets)?;
Подготовка сетки для границ решений
let x_min = -3.0f32;
let x_max = 3.0f32;
let y_min = -3.0f32;
let y_max = 3.0f32;
let step = 0.05f32;
let n_x = (((x_max - x_min) / step) as usize) + 1;
let n_y = (((y_max - y_min) / step) as usize) + 1;
let mut grid = Array2::::zeros((n_x * n_y, 3));
let mut idx = 0;
for i in 0..n_x {
for j in 0..n_y {
let x = x_min as f64 + i as f64 * step as f64;
let y = y_min as f64 + j as f64 * step as f64;
grid.row_mut(idx).assign(&array![x, y, x * y]);
idx += 1;
}
}
let grid_lr_preds = lr_model.predict(&grid);
let grid_kernel = Kernel::params().method(KernelMethod::Gaussian(best_epsilon)).transform_two(&grid, &features);
let grid_svm_preds = svm_model.predict(&grid_kernel);
Пояснения:
- Сетка 3D (с interaction).
- Предсказания на сетке для закрашивания регионов.
Сборка инфографики
let paths = vec!["kmeans_clusters.png", "lr_predictions.png", "svm_predictions.png", "cm_lr.png", "cm_svm.png"];
let mut images = Vec::new();
for path in &paths {
images.push(ImageReader::open(path)?.decode()?);
}
let grid_cols = 3;
let grid_rows = 2;
let cell_width = 800;
let cell_height = 600;
let total_width = grid_cols * cell_width;
let total_height = grid_rows * cell_height;
let mut infographic = ImageBuffer::, Vec>::new(total_width, total_height);
infographic.copy_from(&images[0], 0, 0)?; // KMeans
infographic.copy_from(&images[1], cell_width, 0)?; // LR
infographic.copy_from(&images[2], cell_width * 2, 0)?; // SVM
infographic.copy_from(&images[3], 0, cell_height)?; // CM LR
infographic.copy_from(&images[4], cell_width, cell_height)?; // CM SVM
infographic.save("ml_infographic.png")?;
println!("Infographic generated: ml_infographic.png");
Ok(())
}
Пояснения:
- ImageBuffer — буфер для нового изображения.
- copy_from — копирование частей.
- Сохраняем как PNG.