Найти в Дзене
Один Rust не п...Rust

Инфографика на Rust

[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" 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] =
Оглавление
GitHub - nicktretyakov/infographic

Настройка проекта

  1. Создайте новый проект
  2. Отредактируйте 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.