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

Типобезопасные матрицы с const generics

В машинном обучении (ML) на Rust типобезопасные матрицы с const generics особенно ценны, когда архитектура модели имеет фиксированные размеры, известные на этапе компиляции. Это даёт: Это матрицы (таблицы чисел), где размеры (количество строк и столбцов) проверяются на этапе компиляции (когда программа ещё только собирается). Если вы попытаетесь сложить матрицу 2×2 с матрицей 3×3, компилятор сразу выдаст ошибку — программа даже не запустится. Пример:
Представьте, что у вас есть коробка для яиц на 10 ячеек. Вы не сможете положить туда 12 яиц — коробка просто не даст. Так и с типобезопасными матрицами: если размеры не совпадают, компилятор не даст программе запуститься. Это возможность в языке Rust указывать размеры матриц (или других структур данных) как параметры типа, которые известны на этапе компиляции. То есть, вы можете создать матрицу 3×3 или 4×4, и компилятор будет знать эти размеры заранее. Пример:
Вы пишете функцию для умножения матриц: rustCopyfn multiply<const M: usize, con
Оглавление
ML на RUST без заморочек
Один Rust не п...Rust

В машинном обучении (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 — вектор смещений.

Пошагово:

  1. Умножаем матрицу весов на входной вектор (linear = weights * input).
  2. Прибавляем к каждому элементу результата соответствующий элемент вектора смещений (result[i] += bias[i]).

Пример:
Пусть:

  • weights = [[1, 2], [3, 4], [5, 6]] (3×2),
  • input = [7, 8] (2×1),
  • bias = [10, 20, 30] (3×1).

Тогда:

  1. linear = weights * input = [1*7+2*8, 3*7+4*8, 5*7+6*8] = [23, 53, 83]
  2. 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 дают непревзойдённую безопасность и скорость.