Добавить в корзинуПозвонить
Найти в Дзене
Один Rust не п...Rust

Макросы генерации нейросети

t.me/oneRustnoqRust Для чего нужна данная статья? : Написать нейронную сеть в декларативном стиле, как в Keras, но без runtime-затрат с использованием procedural macros, для реального использования в ML-задачах (без backprop, pooling, batch-обучения, оптимизаторов и т.д.). Зачем Вам это уметь? : Реализовать процедурный макрос, который генерирует реализацию нейронной сети с поддержкой различных слоев, функций активации и базового обучения. Декларативные макросы, такие как vec! или println!, позволяют создавать новый синтаксис для упрощения кода. Они просты в использовании и подходят для задач, связанных с повторяющимся текстом. Процедурные макросы более сложны и мощны. Они включают: Эти категории обеспечивают гибкость для различных сценариев программирования в Rust. Подробную информацию можно найти в официальной документации Rust, например, на страницах Rust By Example: Macros и The Rust Reference: Macros. Декларативные макросы, также известные как макросы по примеру или macro_rules!, я
Оглавление
GitHub - nicktretyakov/neural_network_macro
ML на RUST без заморочек

t.me/oneRustnoqRust

Для чего нужна данная статья? :

Написать нейронную сеть в декларативном стиле, как в Keras, но без runtime-затрат с использованием procedural macros, для реального использования в ML-задачах (без backprop, pooling, batch-обучения, оптимизаторов и т.д.).

Зачем Вам это уметь? :

Реализовать процедурный макрос, который генерирует реализацию нейронной сети с поддержкой различных слоев, функций активации и базового обучения.

Обзор типов макросов

Декларативные макросы, такие как vec! или println!, позволяют создавать новый синтаксис для упрощения кода. Они просты в использовании и подходят для задач, связанных с повторяющимся текстом.

Процедурные макросы более сложны и мощны. Они включают:

  • Функциональные макросы, которые работают как функции и могут принимать переменное количество аргументов (например, sql!(SELECT * FROM posts WHERE id=1)).
  • Derive макросы, которые автоматически реализуют трейты для структур и перечислений (например, #[derive(Debug)]).
  • Атрибутные макросы, которые добавляют пользовательские атрибуты к любому элементу кода (например, #[route(GET, "/")]).

Эти категории обеспечивают гибкость для различных сценариев программирования в Rust.

Поддерживающие ресурсы

Подробную информацию можно найти в официальной документации Rust, например, на страницах Rust By Example: Macros и The Rust Reference: Macros.

Декларативные макросы (Macros by Example)

Декларативные макросы, также известные как макросы по примеру или macro_rules!, являются наиболее простым способом расширения синтаксиса. Они работают аналогично выражению match, где входной код сопоставляется с предопределенными паттернами, и при успешном совпадении заменяется на соответствующий код.

  • Определение: Используется ключевое слово macro_rules! для создания макроса.
  • Примеры: Встроенные макросы, такие как vec! для создания векторов или println! для вывода в консоль.
  • Применение: Подходят для задач, связанных с повторяющимся кодом, например, для создания шаблонов или упрощения синтаксиса.
  • Преимущества: Простота в использовании, минимальные требования к настройке.

Подробности можно найти в Rust By Example: Macros, где приведены примеры, такие как определение макроса say_hello!, который расширяется в вызов println!("Hello!").

Процедурные макросы

Процедурные макросы представляют собой более сложный уровень метапрограммирования, где макросы ведут себя как функции, принимающие токены (фрагменты кода) и возвращающие сгенерированный код. Они требуют определения в отдельном crate с типом proc-macro и могут использоваться только после импорта в другой crate.

Процедурные макросы делятся на три подтипа, каждый из которых имеет свои особенности:

Функциональные процедурные макросы

  • Описание: Работают как вызовы функций, принимая код в качестве аргументов и возвращая сгенерированный код. Они могут обрабатывать переменное количество аргументов, что делает их гибкими.
  • Пример: Макрос sql!(SELECT * FROM posts WHERE id=1), который может генерировать SQL-запросы на основе входных данных.
  • Применение: Подходят для создания DSL (доменных специфичных языков) или генерации сложного кода, например, для работы с базами данных.

Derive макросы (Custom #[derive])

  • Описание: Используются для автоматической реализации трейтов (интерфейсов) для структур и перечислений с помощью атрибута #[derive].
  • Пример: #[derive(Debug)] автоматически реализует трейт Debug для структуры, позволяя выводить её в отладочном формате.
  • Применение: Упрощение работы с трейтами, такими как Clone, Copy, Eq, и другими, без необходимости ручной реализации.
  • Примеры в документации: Подробно описаны в The Rust Programming Language: Macros, с примерами в листингах 20-37, 20-38, 20-40, 20-42, где используются crates syn и quote (версии 2.0 и 1.0).

Атрибутные макросы

  • Описание: Позволяют определять пользовательские атрибуты, которые можно применять к любому элементу кода, такому как функции, структуры или модули. Они более гибкие, чем derive макросы, и могут использоваться в более широком контексте.
  • Пример: #[route(GET, "/")] может быть использован для определения маршрутов в веб-фреймворках, таких как Actix или Rocket.
  • Применение: Подходят для добавления метаданных или генерации кода на основе атрибутов, например, для логирования, сериализации или маршрутизации.

Подробности о процедурных макросах можно найти в The Rust Reference: Procedural Macros, где описаны требования к их определению и ограничения, такие как необходимость использования в корне crate с типом proc-macro.

Макросы с использованием macro_rules!:

макросы, которые работают с синтаксическим деревом, используя шаблоны и подстановочные параметры.

Пример:
macro_rules! say_hello {
() => {
println!("Hello, world!");
};
}

fn main() {
say_hello!();
}

В этом примере макрос vec_of! принимает список значений и создает вектор с этими значениями.

macro_rules! vec_of {

($($x:expr),*) => {

{

let mut temp_vec = Vec::new();

$( temp_vec.push($x);

)*

temp_vec }

};

}

fn main() {

let my_vec = vec_of!(1, 2, 3, 4, 5); println!("{:?}", my_vec);

}

Модульные макросы:

используются в модулях и могут быть вызваны из других модулей. Это полезно для создания макросов, которые должны быть доступными из разных частей вашего кода.

Пример:
// В модуле `my_macros.rs` #[macro_export] macro_rules! my_macro {
($val:expr) => {
println!("Value: {}", $val);
};
}

// В основном файле use my_macros::*;

fn main() {
my_macro!(42);
}

ML макрос define_neural_network

  • Входные данные: Имя сети, размер входного слоя, список слоев (dense или conv2d с параметрами), скорость обучения.
  • Генерация:Структура: Создаются поля для весов и смещений каждого слоя.
    Инициализация: Веса и смещения заполняются случайными значениями.
    Прямой проход: Выполняется последовательный расчет активаций через слои.
    Обучение: Реализован базовый градиентный спуск (для простоты без обратного распространения).

Сложность

  • Разные типы слоев: Поддержка полносвязных (dense) и сверточных (conv2d) слоев.
  • Функции активации: Динамическая генерация кода для ReLU и Sigmoid.
  • Процедурные макросы: Использование syn и quote для парсинга и генерации сложного AST.

Ограничения

  • Упрощенное обучение: нет полного обратного распространения.
  • Conv2d работает только с одним каналом и квадратными входами.
  • Нет проверки типов данных и размеров.

Как используется макрос

  • Создаётся сеть: let mut nn = MyNeuralNet::new(); — макрос сгенерировал метод new(), который инициализирует случайные веса и смещения.
  • Подготавливается входной вектор (784 значения от 0.0 до почти 1.0) и целевой вектор (one-hot: класс 5).
  • Выполняется прямой проход: nn.forward(&input) — сеть последовательно применяет все слои.
  • Затем сеть обучается 100 эпох на одном примере: nn.train(&input, &target).Внутри train сначала делается прямой проход с сохранением активаций каждого слоя.
    Затем выполняется упрощённое обратное распространение ошибки и обновление весов.
  • Периодически выводится MSE-потеря.
  • После обучения снова делается прямой проход — выход должен приблизиться к целевому вектору (значение для класса 5 ≈ 1.0, остальные ≈ 0.0).

Далее создаётся сверточная сеть ConvNet, делается один прямой проход (без обучения).
Затем создаётся тестовая сеть с разными активациями (tanh → sigmoid → relu) и тоже обучается на простом примере.

Как работает сам процедурный макрос

Макрос — это функция, помеченная #[proc_macro], которая на этапе компиляции принимает токены (синтаксис макровызова) и возвращает сгенерированный код.

Шаг 1: Парсинг входа

Используются crates syn и quote.

NeuralNetworkDef — структура, реализующая Parse. Она парсит:

  • Имя сети (MyNeuralNet).
  • input_size: 784.
  • Список слоёв: каждый слой — либо dense(neurons, activation), либо conv2d(filters, kernel_size, activation).
  • learning_rate.

Каждый слой парсится в enum LayerDef.

Шаг 2: Генерация кода

Макрос проходит по всем слоям и для каждого генерирует:

a. Поля структуры

Для dense-слоя:

weights_0: Vec<Vec<f32>>, // матрица weights (neurons × prev_size)
biases_0: Vec<f32>

Для conv2d-слоя:

conv_weights_0: Vec<Vec<Vec<Vec<f32>>>>, // (filters, channels=1, kernel_h, kernel_w)
conv_biases_0: Vec<f32>

b. Инициализация в new()

Веса и смещения инициализируются случайными значениями в диапазоне [-1, 1).
Для первого слоя размер предыдущего слоя = input_size.
Для последующих — берётся количество нейронов/фильтров предыдущего слоя (для conv упрощённо используется #filters * 100 — это заглушка).

c. Прямой проход (forward)

Генерируется код, который последовательно преобразует вектор activations:

Для dense:

for i in 0..neurons {
sum = bias[i] + ∑(activations[j] * weights[i][j])
next_activations.push(activation(sum))
}
activations = next_activations;

Для conv2d (упрощённая реализация):

  • Предполагается, что вход — квадратное изображение (sqrt(784) = 28).
  • Вычисляется свёртка без padding и stride=1, только 1 входной канал.
  • Для каждого фильтра и каждой позиции ядра считается сумма, добавляется bias, применяется активация.
  • Результат «расплющивается» в вектор.

d. Функции активации

Генерируются через вспомогательную функцию generate_activation_function:

  • relu → max(0, x)
  • sigmoid → 1 / (1 + exp(-x))
  • tanh → x.tanh()
  • иначе — линейная (identity)

e. Обучение (train)

Это сильно упрощённая версия backpropagation:

  • Сначала делается прямой проход с сохранением активаций каждого слоя в layer_outputs.
  • Затем для каждого слоя (в обратном порядке) считается ошибка:Для выходного слоя: output[i] - target[i].
    Для скрытых слоёв: очень грубая эвристика (current_output[i] * 0.01 или суммарная ошибка для conv).
  • По градиенту обновляются веса и смещения: weight -= learning_rate * gradient.

Важно: это не настоящий backprop. Производные функций активации не учитываются, ошибки на скрытых слоях вычисляются некорректно. Для conv-слоя обучение ещё более упрощённое (все веса обновляются одинаково на основе средней ошибки).

Шаг 3: Сборка и возврат кода

В конце собирается весь код через quote!:

pub struct MyNeuralNet { ...fields... }

impl MyNeuralNet {
pub fn new() -> Self { ...init... }
pub fn forward(&self, input: &[f32]) -> Vec<f32> { ...layers... }
pub fn train(&mut self, input: &[f32], target: &[f32]) { ...forward + backprop... }
}