В машинном обучении (ML) на Rust типобезопасные матрицы с const generics особенно ценны, когда архитектура модели имеет фиксированные размеры, известные на этапе компиляции. Это даёт:
- Полную проверку размерностей на этапе компиляции (ошибки несоответствия — compile-time errors).
- Нулевой runtime-overhead (как у обычных массивов).
- Идеально для embedded-систем, высокопроизводительного inference и tiny ML (модели на микроконтроллерах).
Это матрицы (таблицы чисел), где размеры (количество строк и столбцов) проверяются на этапе компиляции (когда программа ещё только собирается). Если вы попытаетесь сложить матрицу 2×2 с матрицей 3×3, компилятор сразу выдаст ошибку — программа даже не запустится.
Пример:
Представьте, что у вас есть коробка для яиц на 10 ячеек. Вы не сможете положить туда 12 яиц — коробка просто не даст. Так и с типобезопасными матрицами: если размеры не совпадают, компилятор не даст программе запуститься.
Это возможность в языке Rust указывать размеры матриц (или других структур данных) как параметры типа, которые известны на этапе компиляции. То есть, вы можете создать матрицу 3×3 или 4×4, и компилятор будет знать эти размеры заранее.
Пример:
Вы пишете функцию для умножения матриц:
rustCopyfn multiply<const M: usize, const N: usize, const P: usize>(
a: Matrix<M, N>,
b: Matrix<N, P>
) -> Matrix<M, P> { ... }
Здесь M, N, P — это размеры матриц, известные заранее. Если вы попытаетесь умножить матрицу 2×3 на 4×5, компилятор скажет: "Ошибка! Количество столбцов первой матрицы не равно количеству строк второй!"
Все ошибки, связанные с несовпадением размеров матриц, ловятся ещё до запуска программы. Это экономит время и делает код надёжнее.
Пример:
Если вы напишете код, где пытаетесь сложить матрицу 2×2 с 3×3, компилятор сразу скажет: "Ошибка! Размеры не совпадают!" — и программа не соберётся.
Это значит, что проверка размеров не замедляет программу во время её работы. Всё проверяется заранее, а во время выполнения программа работает так же быстро, как если бы вы использовали обычные массивы.
Пример:
Представьте, что вы проверяете билеты на входе в кино: если все билеты проверены заранее, то на входе никого не задерживают — все проходят быстро.
Такие матрицы особенно полезны для маленьких устройств (например, микроконтроллеров), где важна скорость и экономия памяти. Например, для нейросетей, работающих на микросхемах.
Пример:
Представьте, что у вас есть умный термостат, который предсказывает температуру. Если использовать типобезопасные матрицы, программа будет работать быстро и не будет тратить лишнюю память на проверку размеров во время работы.
Основные сценарии применения в ML
Полносвязные (dense) слои
Самое прямое применение — реализация линейного слоя y = Wx + b.
type Vector<const N: usize> = Matrix<N, 1>;
struct Dense<const IN: usize, const OUT: usize> {
weights: Matrix<OUT, IN>, // веса
bias: Vector<OUT>, // bias как столбец
}
impl<const IN: usize, const OUT: usize> Dense<IN, OUT> {
pub fn forward(&self, input: Vector<IN>) -> Vector<OUT> {
// weights: OUT×IN, input: IN×1 → результат OUT×1
let linear = self.weights.clone() * input;
// Прибавляем bias (broadcast по столбцам)
let mut result = linear;
for i in 0..OUT {
result.data[i][0] += self.bias.data[i][0];
}
result
}
}
Vector<const N: usize> Это псевдоним (alias) для матрицы-столбца размером N×1. То есть, вектор длины N.
Пример:
Vector<3> — это столбец из трёх чисел, например:
Copy[ 1 ]
[ 2 ]
[ 3 ]
Структура Dense Это описание полносвязного (dense) слоя нейронной сети. У него есть:
- weights — матрица весов размером OUT×IN (OUT строк, IN столбцов).
- bias — вектор смещений (bias) размером OUT×1.
Пример:
Если Dense<2, 3>, то:
- weights — матрица 3×2 (3 строки, 2 столбца).
- bias — вектор из 3 чисел.
Метод forward Вычисляет выход слоя по формуле: y = Wx + b, где:
- W — матрица весов,
- x — входной вектор,
- b — вектор смещений.
Пошагово:
- Умножаем матрицу весов на входной вектор (linear = weights * input).
- Прибавляем к каждому элементу результата соответствующий элемент вектора смещений (result[i] += bias[i]).
Пример:
Пусть:
- weights = [[1, 2], [3, 4], [5, 6]] (3×2),
- input = [7, 8] (2×1),
- bias = [10, 20, 30] (3×1).
Тогда:
- linear = weights * input = [1*7+2*8, 3*7+4*8, 5*7+6*8] = [23, 53, 83]
- result = [23+10, 53+20, 83+30] = [33, 73, 113]
Многослойный перцептрон (MLP) с фиксированной архитектурой
Можно построить сеть с полностью известными размерами слоёв.
struct MLP<const IN: usize, const H1: usize, const H2: usize, const OUT: usize> {
layer1: Dense<IN, H1>,
layer2: Dense<H1, H2>,
layer3: Dense<H2, OUT>,
}
// Пример: сеть 784 → 128 → 64 → 10 (классификатор MNIST-like)
type MnistMLP = MLP<784, 128, 64, 10>;
Forward-pass будет полностью типобезопасным: компилятор проверит совместимость всех слоёв.
Многослойный перцептрон (MLP) — это простейшая нейронная сеть, где слои нейронов соединены последовательно: выход одного слоя — вход для следующего. Используется для классификации, регрессии и других задач.
Пример:
Представь, что ты сортируешь фрукты. Первый слой определяет цвет, второй — форму, третий — выдаёт название (яблоко, банан и т.д.).
Фиксированная архитектура Здесь размеры всех слоёв известны заранее и не меняются. Например, сеть всегда принимает 784 входа, затем 128 нейронов, потом 64, и выдаёт 10 выходов.
Пример:
Если ты строишь дом, то заранее знаешь: 3 комнаты, 2 окна, 1 дверь — и не будешь менять это во время строительства.
struct MLP<const IN: usize, const H1: usize, const H2: usize, const OUT: usize> Это описание структуры (типа данных) на языке Rust, где размеры слоёв (IN, H1, H2, OUT) задаются как константы на этапе компиляции (не во время выполнения программы).
Пример:
Ты заказываешь пиццу и говоришь: "Мне пиццу с 4 кусками, 2 видами сыра, 3 видами начинки". Все числа известны заранее.
Dense<IN, H1> — это слой нейронов, где каждый нейрон предыдущего слоя соединён со всеми нейронами следующего. IN — размер входа, H1 — размер выхода.
Пример:
Если у тебя 3 друга (IN=3), и ты хочешь отправить открытку каждому из 5 родственников (H1=5), то каждый друг получит по 5 открыток.
type MnistMLP = MLP<784, 128, 64, 10>; Создаётся новый тип MnistMLP — сеть с фиксированными размерами: 784 входа, 128 нейронов в первом скрытом слое, 64 — во втором, и 10 выходов.
Пример:
Ты называешь свой новый велосипед "МойВелосипед" и говоришь: "У него 2 колеса, 3 скорости, красный цвет".
Типобезопасный forward-pass Компилятор проверяет, что размеры слоёв совпадают. Если где-то ошибка (например, передали 128 вместо 784), программа не скомпилируется.
Пример:
Ты не сможешь вставить круглую вилку в квадратную розетку — компилятор не даст.
Итоговый пример
Представь, что ты строишь завод по производству игрушек:
- MLP — сам завод.
- 784 → 128 → 64 → 10 — количество станков на каждом этапе.
- Типобезопасность — если где-то не хватает деталей, завод просто не запустится.
Активации и нелинейности
Реализуются как функции, работающие с фиксированными векторами.
fn relu<const N: usize>(v: Vector<N>) -> Vector::Vector<N> {
let mut result = v;
for i in 0..N {
result.data[i][0] = v.data[i][0].max(0.0);
}
result
}
const N: usize — Const Generics (Константные дженерики) Это способ создать функцию или структуру, которая работает с фиксированным размером, известным на этапе компиляции. Здесь N — это константа, которая задаёт размер вектора.
Пример:
Представьте, что вы хотите создать коробку, в которую всегда кладут ровно 3 яблока. Вы можете сказать: "Сделай коробку для 3 яблок". Здесь "3" — это константа, известная заранее.
В коде:
rustCopyfn relu<const N: usize>(...) // N — это размер вектора, известный при компиляции
Vector<N> — Вектор фиксированного размера Это структура данных, которая хранит ровно N чисел (например, вектор из 3 чисел: [1.0, 2.0, 3.0]). Размер вектора фиксирован и не может измениться.
Пример:
Если N = 2, то Vector<2> — это пара чисел, например, [5.0, -3.0].
fn relu<const N: usize>(v: Vector<N>) -> Vector<N> — Функция активации ReLU (Rectified Linear Unit) — это простая функция, которая заменяет все отрицательные числа в векторе на ноль, а положительные оставляет без изменений.
Пример:
Если на входе вектор [1.0, -2.0, 3.0], то на выходе будет [1.0, 0.0, 3.0].
v.data[i][0] — Доступ к элементам вектора Вектор хранит свои числа в массиве data. Здесь v.data[i][0] — это i-й элемент вектора.
Пример:
Если v.data = [[1.0], [2.0], [3.0]], то v.data[1][0] = 2.0.
v.data[i][0].max(0.0) — Замена отрицательных чисел на ноль Эта операция сравнивает число с нулём и выбирает максимальное значение. Если число отрицательное, то выбирается 0.0.
Пример:
- (-2.0).max(0.0) = 0.0
- (5.0).max(0.0) = 5.0
Итоговый пример работы функции:
rustCopylet v = Vector { data: [[1.0], [-2.0], [3.0]] };
let result = relu(v);
// result.data = [[1.0], [0.0], [3.0]]
Такие функции часто используются в нейронных сетях для обработки данных. Const generics позволяют оптимизировать код, потому что размер вектора известен заранее.
Обучение (backpropagation) для маленьких моделей
Для простых задач можно реализовать градиентный спуск вручную — все размеры проверяются статически.
// Пример обновления весов
self.weights = self.weights - (learning_rate * grad_weights);
(где - и * скаляр реализуются через трейты)
Обучение нейронной сети — это процесс, при котором модель "учится" на примерах, чтобы делать правильные предсказания. Backpropagation (обратное распространение ошибки) — это алгоритм, который помогает модели понять, как именно ей нужно изменить свои внутренние параметры (веса), чтобы ошибка предсказания стала меньше.
Пример:
Представьте, что вы учите ребёнка отличать кошек от собак. Сначала он ошибается, но вы каждый раз говорите: "Нет, это кошка!" — и он постепенно запоминает признаки. Так и нейросеть: она получает примеры, сравнивает свои ответы с правильными, и backpropagation помогает ей "понять", как исправить ошибки.
Градиентный спуск - Это метод оптимизации, который помогает найти такие значения весов модели, при которых ошибка минимальна. Представьте, что вы стоите на холме и хотите спуститься в самую низкую точку. Вы смотрите, куда круче склон, и делаете маленький шаг в том направлении. Градиентный спуск работает так же: он показывает, в каком направлении нужно изменить веса, чтобы ошибка уменьшилась.
Пример:
Если модель предсказала, что на картинке собака, а там кошка, градиентный спуск подскажет, насколько и в какую сторону нужно изменить веса, чтобы в следующий раз ошибка была меньше.
Реализация градиентного спуска вручную - В маленьких моделях можно самому написать код, который будет обновлять веса по формуле:
Copyself.weights = self.weights - (learning_rate * grad_weights);
- self.weights — текущие веса модели.
- grad_weights — градиент (направление и величина изменения весов).
- learning_rate — шаг, насколько сильно мы меняем веса за один раз.
Пример:
Если learning_rate = 0.01, а grad_weights = 2.0, то веса изменятся на 0.01 * 2.0 = 0.02 в сторону уменьшения.
Трейты для операций (- и ) — это способ описать, какие операции можно делать с объектами. Здесь говорится, что для матриц реализованы операции вычитания (-) и умножения на скаляр (*), чтобы можно было писать код в естественном виде.
Пример:
Copylet new_weights = weights - (0.01 * grad);
Здесь - и * — это операции, которые реализованы через трейты.
Представление батчей
Если батч фиксированный (например, batch_size=1 или batch_size=32), можно сделать BatchMatrix<const BATCH: usize, const ROWS: usize, const COLS: usize>.
Батч — это группа данных, которую обрабатывают вместе, а не по одному элементу. В машинном обучении и программировании часто используют батчи, чтобы ускорить вычисления.
Пример:
Представьте, что у вас есть 100 фотографий, и вы хотите их все уменьшить. Вместо того, чтобы обрабатывать каждую фотографию по одной, вы берёте сразу 32 фотографии (батч) и уменьшаете их все вместе. Так быстрее!
Фиксированный батч (fixed batch_size) — это когда размер группы данных всегда одинаковый. Например, всегда по 1, 32 или 64 элемента.
Пример:
Если вы всегда уменьшаете фотографии группами по 32 штуки, то ваш батч фиксированный с размером 32.
Const generics (константные дженерики) - Это возможность в языке Rust указывать конкретные числовые значения (например, размеры) прямо в типе данных. Это помогает компилятору лучше оптимизировать код и избегать ошибок.
Пример:
Вместо того, чтобы писать функцию, которая работает с любым размером матрицы, вы можете создать тип BatchMatrix<32, 10, 20>, который всегда будет работать с батчем из 32 матриц размером 10x20.
BatchMatrix<const BATCH: usize, const ROWS: usize, const COLS: usize> - Это тип данных, который хранит несколько матриц (батч) фиксированного размера. Здесь:
- BATCH — сколько матриц в группе (например, 32),
- ROWS — сколько строк в каждой матрице (например, 10),
- COLS — сколько столбцов в каждой матрице (например, 20).
Пример:
Если у вас есть BatchMatrix<2, 3, 4>, это значит:
- В группе 2 матрицы,
- Каждая матрица имеет 3 строки и 4 столбца.
Специализированные маленькие модели Линейная/логистическая регрессия с фиксированным числом фич.
Tiny нейросети для embedded (например, классификация ключевых слов, жестов).
Модели для микроконтроллеров (no_std + const generics = максимальная производительность).
Это небольшие по размеру и ресурсам алгоритмы машинного обучения, которые решают узкие задачи (например, распознавание жестов или ключевых слов).
Пример:
Представьте умные наушники, которые распознают только две команды: «Включи музыку» и «Выключи музыку». Им не нужен мощный компьютер — достаточно маленькой модели, которая быстро и эффективно выполняет свою задачу.
Линейная/логистическая регрессия с фиксированным числом фич - Это простые алгоритмы машинного обучения:
- Линейная регрессия предсказывает числовые значения (например, цену дома по его площади).
- Логистическая регрессия классифицирует данные (например, определяет, спам письмо или нет).
Фиксированное число фич — значит, что модель заранее знает, сколько параметров (фич) она будет использовать (например, только площадь и количество комнат, не больше).
Пример:
Модель для умных часов, которая по двум датчикам (пульс и движение) определяет, идёт человек или бежит.
Tiny нейросети для embedded - Это очень маленькие нейронные сети, которые работают на устройствах с ограниченными ресурсами (например, микроконтроллеры).
Пример:
Умный выключатель света, который распознаёт хлопок в ладоши. Ему не нужен мощный процессор — достаточно крошечной нейросети, которая быстро обрабатывает звук.
Модели для микроконтроллеров (no_std + const generics)
- Микроконтроллер — это маленький компьютер внутри устройства (например, в стиральной машине или пульте).
- no_std — значит, модель не использует стандартную библиотеку языка (чтобы занимать меньше места).
- const generics — это техника в программировании, которая позволяет заранее задать размеры данных (например, количество фич), чтобы код работал быстрее и надёжнее.
Пример:
Модель для умного термостата, которая по трём датчикам (температура, влажность, время суток) регулирует отопление. Она написана так, чтобы занимать минимум памяти и работать без сбоев.
Существующие библиотеки, активно использующие const generics в ML
- dfdx — современная (2023–2026) deep learning библиотека, построенная вокруг const generics. Размеры тензоров — часть типа. Пример:
type Model = (Linear<784, 128>, ReLU, Linear<128, 10>);
let model = Model::build(dev);
let pred = model.forward(x);
- Поддерживает автоград, CPU/GPU (через CUDA), фиксированные и динамические формы.
- burn — ещё один мощный фреймворк с backend-ами (tch, wgpu, ndarray). Поддерживает const generics для статических форм.
- glam — для векторов/матриц в графике, часто используется в ML-задачах с 3D-данными (pose estimation, robotics).
Когда это особенно выгодно
- Embedded ML / TinyML: модели на STM32, RP2040, ESP32 — где динамические аллокации недопустимы.
- High-performance inference: без overhead динамических проверок.
- Безопасность кода: в production-системах (автопилот, финансы) ошибки размерностей невозможны в runtime.
Если нужны динамические размеры (большие модели, переменный batch) — лучше ndarray/tch-rs/polars, но для фиксированной архитектуры const generics дают непревзойдённую безопасность и скорость.