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

CMake для TensorFlow с C++

t.me/oneRustnoqRust Если проект на 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=имя_библиотеки");
} Сначала необходимо настроить 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: {}", re
Оглавление
nicktretyakov1/cmake_ml | Gitverse
ML на RUST без заморочек

t.me/oneRustnoqRust

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

  • Научиться использовать 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.