Для чего нужна данная статья? :
- Научиться использовать CMake для быстрого обучения нейронной сети MLP с использованием TensorFlow, при работе с системами сборки, которые требуют интеграции с C++ кодом.
Зачем Вам это уметь? :
1. Использование CMake в build.rs
Если проект на Rust зависит от C/C++ кода, то в файле build.rs (скрипт сборки для Cargo) можно вызывать CMake для сборки сторонних C/C++ библиотек:
Добавление CMake в зависимостях:
В Cargo.toml нужно добавить зависимость на cmake crate:
[build-dependencies] cmake = "0.1"
Пример использования cmake crate в build.rs:
extern crate cmake;
use cmake::Config;
fn main() {
let dst = Config::new("путь_к_проекту_C").build();
println!("cargo:rustc-link-search=native={}/lib", dst.display());
println!("cargo:rustc-link-lib=static=имя_библиотеки");
}
2. Сборка C/C++ библиотеки с помощью CMake и последующая её интеграция через FFI
Сначала необходимо настроить CMake для сборки C/C++ библиотеки.
В Rust с помощью FFI можно подключить и использовать скомпилированную библиотеку:
Использование библиотеки через FFI:
#[link(name = "имя_библиотеки")] extern "C" {
// объявление функций, которые импортируются из C/C++ fn c_function(x: i32) -> i32;
}
fn main() {
unsafe {
let result = c_function(10);
println!("Result from C: {}", result);
}
}
3. Встраивание Rust в проекты на C/C++ с помощью CMake
В некоторых случаях может быть обратная задача: вы хотите встроить код на Rust в существующий CMake-проект на C/C++. Для этого создаются CMake-скрипты, которые вызывают Cargo для компиляции кода на Rust, а затем подключают скомпилированную библиотеку:
Использование Cargo в CMake:
find_package(Cargo REQUIRED)
add_custom_target(
RustLib ALL
COMMAND cargo build --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/rust_code
COMMENT "Building Rust code"
)
add_library(my_rust_lib STATIC IMPORTED)
set_target_properties(my_rust_lib PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/rust_code/target/release/librust_code.a
)
add_dependencies(my_rust_lib RustLib)
4. Cross-compilation
В некоторых случаях можно использовать CMake для кросс-компиляции проектов на C/C++, которые затем связываются с Rust. Для этого настраиваются соответствующие конфигурации CMake и сборка через build.rs в зависимости от таргета, на который компилируется Rust.
5. Использование Bindgen для автогенерации FFI
Иногда нужно автоматически генерировать привязки (bindings) для C/C++ библиотек. Для этого используется crate bindgen, но для генерации некоторых привязок может потребоваться предварительная сборка C/C++ кода с помощью CMake.
Общая структура проекта
MNIST — это популярный набор данных для обучения нейросетей. Он содержит 70 000 чёрно-белых изображений рукописных цифр (от 0 до 9), каждое размером 28×28 пикселей.
- mnist_loader.rs — модуль для загрузки и подготовки датасета MNIST.
Скачивает или читает эти картинки с цифрами.
Преобразует их в удобный для программы формат (например, в числа, которые может понять нейросеть).
Представьте, что у вас есть папка с фотографиями цифр. Этот модуль берёт каждую фотографию, превращает её в набор чисел (пиксели) и передаёт дальше для обучения.
- main.rs — основной файл: Демонстрирует FFI-вызов C++ функции.
Представьте, что у вас есть два друга: один говорит только по-русски, а другой — только по-английски. FFI — это переводчик между ними. Здесь Rust вызывает функции, написанные на C++, чтобы использовать возможности TensorFlow.
Если в C++ написана функция, которая строит нейросеть, то Rust может её вызвать через FFI, чтобы не писать всё заново.
Загружает данные.
Строит граф TensorFlow (простая полносвязная сеть MLP).
TensorFlow — это библиотека для машинного обучения. Граф — это описание вычислений, которые нужно выполнить. MLP (Multi-Layer Perceptron) — это простая нейросеть, где каждый слой связан со всеми нейронами предыдущего слоя.
Программа создаёт "чертеж" нейросети: как данные будут проходить через слои нейронов, чтобы научиться распознавать цифры.
Представьте, что вы строите завод по производству печенья. Граф — это схема, по которой тесто проходит через разные этапы (слои), пока не станет печеньем.
Обучает модель.
Обучение модели — это процесс, при котором нейросеть "смотрит" на примеры (цифры из MNIST) и учится их распознавать.
Программа показывает нейросети тысячи картинок с цифрами и говорит: "Это цифра 5, это цифра 7..." — пока нейросеть не научится сама их распознавать.
Как ребёнок учится отличать кошку от собаки: ему показывают много картинок и говорят, что на них изображено.
Вычисляет точность на тестовом наборе.
После обучения нейросеть проверяют на новых данных, которые она не видела раньше. Точность — это процент правильных ответов.
Программа даёт нейросети новые картинки с цифрами и смотрит, сколько из них она распознаёт правильно.
Как экзамен в школе: если ученик правильно ответил на 90 из 100 вопросов, его точность — 90%.
Программа обучает нейронную сеть распознавать рукописные цифры (MNIST) и достигает ~97–98% точности за 10 эпох.
src/lib/mnist_loader.rs — загрузка данных
- Crate mnist автоматически скачивает датасет (если его нет) и предоставляет его в виде векторов.
- Поля trn_img, trn_lbl, tst_img, tst_lbl — это стандартные имена в crate mnist = "0.5.0".
Это стандартные названия переменных в библиотеке mnist:
trn_img — изображения для обучения (train images)
trn_lbl — метки (цифры) для обучающих изображений (train labels)
tst_img — изображения для тестирования (test images)
tst_lbl — метки для тестовых изображений (test labels)
- Изображения — плоские векторы длиной 784 (28×28).
Каждое изображение в MNIST — это чёрно-белая картинка 28×28 пикселей. Чтобы удобнее было обрабатывать, её "распрямляют" в один длинный вектор из 784 чисел (28×28=784).
Представьте, что у вас есть таблица 28 на 28 ячеек, где каждое число — это яркость пикселя (от 0 до 255). Чтобы передать эту таблицу в нейросеть, её превращают в одну строку из 784 чисел.
- Метки преобразуются в one-hot для softmax cross-entropy.
Метка — это правильный ответ (например, цифра 3). Чтобы нейросеть могла с ней работать, метку кодируют в виде вектора, где все элементы — 0, кроме одного, который равен 1.
Пример:
Если метка — цифра 3, то one-hot вектор будет:
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
(всего 10 позиций, по одной на каждую цифру от 0 до 9).
Это нужно для функции потерь softmax cross-entropy, которая помогает нейросети учиться.
src/main.rs — основная логика
- Импортируем всё необходимое из tensorflow.
- Подключаем наш модуль с данными.
FFI (интеграции C++)
#[link(name = "example_cpp_lib")]
unsafe extern "C" {
fn c_function(x: i32) -> i32;
}
unsafe {
let result = c_function(10);
println!("Result from C++: {}", result); // Выводит 20
}
- #[link] говорит линкеру подключить статическую библиотеку example_cpp_lib (собранную через CMake в build.rs).
- unsafe extern "C" — объявление внешней функции из C++.
- Вызов в unsafe-блоке, потому что FFI потенциально опасен.
Загрузка данных
- Получаем структуру с подготовленными векторами изображений и меток.
Построение графа TensorFlow
- Graph — контейнер для всех операций.
- Scope — область видимости для имён операций (помогает в отладке).
Placeholders (входные данные)
let x = ops::Placeholder::new()
.dtype(DataType::Float)
.shape([-1, 28, 28, 1]) // batch_size × 28 × 28 × 1
.build(&mut scope.new_sub_scope("input"))?;
let y = ops::Placeholder::new()
.dtype(DataType::Float)
.shape([-1, 10]) // batch_size × 10 (one-hot)
.build(&mut scope.new_sub_scope("input"))?;
- -1 означает динамический размер батча.
Модель (вызывается функция build_model)
- Простая MLP: 784 → 512 (ReLU) → 10.
- Возвращает:logits — выход сети (до softmax).
variables — список всех обучаемых переменных (веса и биасы).
weights — только веса (для L2-регуляризации).
Внутри build_model:
- reshape превращает вход в плоский вектор [batch, 784].
- Variable::builder().const_initial_value(...) — создаёт переменные с постоянной инициализацией (маленькие случайные значения).
- mat_mul + add + relu — стандартные операции.
- Все операции добавляются в граф через &mut scope.
Loss и регуляризация
let cross_entropy = ops::softmax_cross_entropy_with_logits(logits.clone(), y.clone(), &mut scope)?.0;
let cross_entropy_mean = ops::reduce_mean(...);
// L2 вручную суммируем квадраты всех весов
let mut l2_loss = ops::constant(0.0f32, &mut scope)?;
for w in &weights {
l2_loss = ops::add(l2_loss, ops::reduce_sum(ops::square(w.clone(), &mut scope)?, &[], &mut scope)?, &mut scope)?;
}
let loss = ops::add(cross_entropy_mean, lambda * l2_loss, &mut scope)?;
Оптимизатор
В старой версии нет Adam → используем простой SGD с большим learning rate (0.5).
Точность
Сравниваем предсказания с истинными метками, считаем долю правильных.
Запуск сессии
- SessionRunArgs — правильный способ указать, что запускать и какие тензоры получать.
Цикл обучения
- Батчи по 100 примеров.
- Формируем тензоры x_batch и y_batch.
- SessionRunArgs:add_feed — подаём входные данные.
add_fetch — указываем, что нужно выполнить (train_op). - Для теста аналогично, но запрашиваем accuracy и берём значение через fetch.