ndarray для Rust: полное руководство по работе с N-мерными массивами
1. Что это и зачем нужна
Библиотека ndarray — это стандартное расширение экосистемы Rust для работы с многомерными массивами (N-мерными тензорами). Она предоставляет мощные, типобезопасные и производительные структуры данных для численных вычислений, линейной алгебры, обработки сигналов, машинного обучения и научных расчётов. Если вы знакомы с NumPy в Python, то ndarray — это его прямой аналог в мире Rust, но с преимуществами строгой типизации, отсутствия сборщика мусора и контроля над памятью.
Зачем она нужна? В Rust нет встроенной поддержки многомерных массивов с произвольным количеством осей. Стандартные векторы (Vec<T>) и срезы (&[T]) позволяют работать только с одномерными данными. Для задач, где требуется матричная алгебра, свёртки, тензорные операции или работа с изображениями, необходима специализированная библиотека. ndarray решает эти проблемы, предоставляя:
- Типизированные N-мерные массивы (
ArrayBase) с фиксированным или динамическим числом осей. - Эффективные срезы (slicing), индексацию и итерацию.
- Векторизованные арифметические операции (broadcasting).
- Интеграцию с BLAS/LAPACK через
ndarray-linalgдля высокопроизводительных линейно-алгебраических вычислений. - Поддержку параллельных вычислений через
rayon.
Библиотека активно используется в таких проектах, как RustML, Linfa (фреймворк машинного обучения), Image (обработка изображений) и многих других. Она является фундаментом для научных вычислений в Rust.
2. Установка
Для установки ndarray используйте менеджер пакетов Cargo, входящий в состав Rust. Добавьте зависимость в файл Cargo.toml вашего проекта:
[dependencies]
ndarray = "0.16"
Затем выполните команду в терминале:
cargo build
Для работы с линейной алгеброй (матричные умножения, собственные значения) добавьте ndarray-linalg и выберите бэкенд BLAS (например, OpenBLAS или Intel MKL):
[dependencies]
ndarray = "0.16"
ndarray-linalg = { version = "0.16", features = ["openblas"] }
Для параллельных итераций используйте ndarray-parallel:
[dependencies]
ndarray = "0.16"
ndarray-parallel = "0.16"
3. Быстрый старт — минимальный рабочий пример
Создадим одномерный массив, выполним поэлементное сложение и выведем результат. Сохраните следующий код в файл src/main.rs:
use ndarray::Array;
fn main() {
// Создаём одномерный массив из 5 элементов
let a = Array::from_vec(vec![1, 2, 3, 4, 5]);
let b = Array::from_vec(vec![10, 20, 30, 40, 50]);
// Поэлементное сложение
let c = &a + &b;
println!("a = {:?}", a);
println!("b = {:?}", b);
println!("a + b = {:?}", c);
}
Запустите программу:
cargo run
Вывод:
a = [1, 2, 3, 4, 5], shape=[5], strides=[1], layout=CFcf (0x1)
b = [10, 20, 30, 40, 50], shape=[5], strides=[1], layout=CFcf (0x1)
a + b = [11, 22, 33, 44, 55], shape=[5], strides=[1], layout=CFcf (0x1)
4. Основные методы и функции (минимум 8)
4.1. Array::from_shape_vec
Сигнатура: pub fn from_shape_vec<Sh>(shape: Sh, v: Vec<A>) -> Result<ArrayBase<OwnedRepr<A>, Dim<Sh::Repr>>, ShapeError>
Что делает: Создаёт N-мерный массив из одномерного вектора, интерпретируя его как данные, уложенные в порядке C (row-major). Возвращает Result, так как размер вектора должен совпадать с произведением размерностей.
use ndarray::Array;
fn main() {
// Создаём матрицу 2x3 из вектора
let a = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
println!("{:?}", a);
// Вывод: [[1, 2, 3],
// [4, 5, 6]]
}
4.2. .shape()
Сигнатура: pub fn shape(&self) -> &Self::Dim
Что делает: Возвращает ссылку на кортеж, представляющий размерности массива по каждой оси.
use ndarray::Array;
fn main() {
let a = Array::range(0., 12., 1.).into_shape((3, 4)).unwrap();
println!("Shape: {:?}", a.shape()); // Shape: [3, 4]
}
4.3. .sum()
Сигнатура: pub fn sum(&self) -> A (где A — тип элемента, реализующий Add и Zero)
Что делает: Вычисляет сумму всех элементов массива.
use ndarray::Array;
fn main() {
let a = Array::from_vec(vec![1, 2, 3, 4, 5]);
let total = a.sum();
println!("Sum: {}", total); // Sum: 15
}
4.4. .mapv()
Сигнатура: pub fn mapv<B, F>(&self, f: F) -> ArrayBase<OwnedRepr<B>, Dim<Ix1>> (для 1D) или обобщённо для N-мерных: pub fn mapv_into<B, F>(self, f: F) -> ArrayBase<OwnedRepr<B>, Self::Dim>
Что делает: Применяет функцию f к каждому элементу массива и возвращает новый массив с результатами.
use ndarray::Array;
fn main() {
let a = Array::from_vec(vec![1.0, 2.0, 3.0]);
let b = a.mapv(|x| x * 2.0 + 1.0);
println!("{:?}", b); // [3, 5, 7]
}
4.5. .dot() (скалярное произведение)
Сигнатура: pub fn dot<S2>(&self, rhs: &ArrayBase<S2, Ix1>) -> A (для 1D) или pub fn dot<S2>(&self, rhs: &ArrayBase<S2, Ix2>) -> ArrayBase<OwnedRepr<A>, Ix2> (для 2D — матричное умножение)
Что делает: Вычисляет скалярное произведение двух одномерных массивов или матричное произведение двух двумерных массивов.
use ndarray::Array;
fn main() {
let a = Array::from_vec(vec![1, 2, 3]);
let b = Array::from_vec(vec![4, 5, 6]);
let dot_product = a.dot(&b);
println!("Dot product: {}", dot_product); // 1*4 + 2*5 + 3*6 = 32
// Матричное умножение
let m1 = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
let m2 = Array::from_shape_vec((3, 2), vec![7, 8, 9, 10, 11, 12]).unwrap();
let result = m1.dot(&m2);
println!("Matrix product:\n{:?}", result);
// [[58, 64],
// [139, 154]]
}
4.6. .slice()
Сигнатура: pub fn slice<I>(&self, info: I) -> ArrayView<A, I::OutDim> где I — кортеж срезов Si или s![...]
Что делает: Создаёт представление (view) на подмассив без копирования данных. Поддерживает шаги и диапазоны.
use ndarray::{Array, s};
fn main() {
let a = Array::range(0., 16., 1.).into_shape((4, 4)).unwrap();
// Берём подматрицу: строки 1..3, столбцы 1..3
let view = a.slice(s![1..3, 1..3]);
println!("{:?}", view);
// [[5, 6],
// [9, 10]]
}
4.7. .fold()
Сигнатура: pub fn fold<B, F>(&self, init: B, f: F) -> B
Что делает: Свёртка (редукция) массива с заданным начальным значением и бинарной операцией. Порядок обхода — row-major.
use ndarray::Array;
fn main() {
let a = Array::from_vec(vec![1, 2, 3, 4]);
// Вычисляем произведение всех элементов
let product = a.fold(1, |acc, &x| acc * x);
println!("Product: {}", product); // 24
}
4.8. Array::eye()
Сигнатура: pub fn eye(n: usize) -> Array2<A> (где A — тип с плавающей точкой или целый, реализующий One и Zero)
Что делает: Создаёт единичную матрицу размером n x n.
use ndarray::Array;
fn main() {
let identity = Array::eye::<f64>(3);
println!("{:?}", identity);
// [[1, 0, 0],
// [0, 1, 0],
// [0, 0, 1]]
}
4.9. .t() (транспонирование)
Сигнатура: pub fn t(&self) -> ArrayView<A, Ix2>
Что делает: Возвращает транспонированное представление двумерного массива (строки становятся столбцами). Не копирует данные.
use ndarray::Array;
fn main() {
let a = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
let b = a.t();
println!("Original:\n{:?}\nTransposed:\n{:?}", a, b);
// Original:
// [[1, 2, 3],
// [4, 5, 6]]
// Transposed:
// [[1, 4],
// [2, 5],
// [3, 6]]
}
4.10. Array::from_iter()
Сигнатура: pub fn from_iter<I>(iterable: I) -> ArrayBase<OwnedRepr<A>, Ix1>
Что делает: Создаёт одномерный массив из итератора.
use ndarray::Array;
fn main() {
let squares: Array<i32, _> = (0..5).map(|x| x * x).collect();
println!("{:?}", squares); // [0, 1, 4, 9, 16]
}
5. Реальный практический пример (100-200 строк кода)
Рассмотрим задачу: реализовать нормализацию изображения (матрицы пикселей) методом Min-Max scaling и применить свёртку (blur) с помощью ядра 3x3. Пример демонстрирует работу с 2D массивами, срезами, broadcasting и поэлементными операциями.
use ndarray::{Array, Array2, s};
use std::f64;
/// Нормализация Min-Max: приводит значения к диапазону [0, 1]
fn