Привет, Хабр!

Работа с матрицами в стандартном C++ — это боль, страдание и масса ненужного кода. Почему? Потому что стандартная библиотека STL вообще не предназначена для линейной алгебры. Например, если вы хотите умножить две матрицы, вам придётся писать кастомный алгоритм, отлаживать его, а затем ещё раз писать его, когда поймёте, что оптимизация не та.

Но есть библиотека Armadillo — и с ней всё иначе. Вот её главные фичи:

  1. Простота: вы работаете с матрицами, как с обычными переменными. Никаких «джунглей» из циклов.

  2. Скорость: поддержка OpenBLAS, MKL и LAPACK делает вычисления молниеносными.

  3. Возможности: встроенные методы для кластеризации, PCA, разреженных матриц и даже машинного обучения.

  4. Универсальность: можно работать как с плотными, так и с разреженными данными.

Теперь перейдём синтаксису.

Базовый синтаксис Armadillo

Для работы с Armadillo вам нужно подключить её заголовочный файл:

#include <armadillo>

Чтобы избежать постоянного написания arma::, используйте пространство имён:

using namespace arma;

Вот и всё. Теперь все готово к работе с матрицами, векторами и другими элементами библиотеки.

Создание матриц

Самый популярный класс в Armadillo — это mat. Он используется для работы с плотными матрицами.

Создание пустой матрицы:

mat A;  // Пока без размеров

Создание матрицы фиксированного размера:

mat A(3, 4);  // Матрица 3x4, заполненная мусором (до Armadillo 10.4) или нулями (с версии 10.5).

Заполнение значениями:

  • Случайные числа (равномерное распределение):

mat A(3, 3, fill::randu);  // Заполнена случайными числами от 0 до 1
  • Случайные числа (нормальное распределение):

mat A(3, 3, fill::randn);  // Заполнена числами с нормальным распределением
  • Единичная матрица:

mat I = eye<mat>(4, 4);  // Единичная матрица 4x4
  • Матрица из нулей или единиц:

mat Z = zeros<mat>(5, 5);  // Матрица 5x5, заполненная нулями
mat O = ones<mat>(5, 5);   // Матрица 5x5, заполненная единицами

Доступ к элементам

Можно получить доступ к элементам матрицы так же, как в Python или MATLAB:

mat A(3, 3, fill::randu);
double value = A(0, 1);  // Элемент из первой строки и второго столбца
A(1, 2) = 3.14;          // Присваиваем значение

Для доступа без проверки границ используйте .at():

double value = A.at(0, 1);  // Чуть быстрее, но без проверки на выход за пределы

Основные операции с матрицами

Транспонирование:

mat B = A.t();

Сложение/вычитание матриц:

mat C = A + B;  // Элементы складываются поэлементно
mat D = A - B;  // Аналогично

Умножение матриц:

mat E = A * B;  // Классическое умножение матриц

Элементарное (поэлементное) умножение:

mat F = A % B;  // Каждое значение A(i, j) умножается на B(i, j)

Деление элементов:

mat G = A / 2.0;  // Деление всех элементов на 2

Векторы

В векторы представлены классами vec и rowvec.

Создание вектора:

vec x = {1.0, 2.0, 3.0};  // Столбец
rowvec y = {4.0, 5.0, 6.0};  // Строка

Доступ к элементам:

x(1) = 42.0;  // Присваиваем значение второму элементу

Основные операции:

  • Нормализация:

x = normalise(x);  // Приводит длину вектора к единице
  • Скалярное произведение:

double result = dot(x, y);

Работа с кубами

cube— это трёхмерные массивы, которые используются в задачах обработки изображений или временных рядов.

Создание куба:

cube C(4, 4, 3, fill::randu);  // 4x4x3 массив со случайными числами

Доступ к элементам:

C(0, 1, 2) = 3.14;  // Элемент в 0-й строке, 1-м столбце, 2-м "срезе"

Срезы куба:

mat slice1 = C.slice(0);  // Извлекаем первый "срез" куба (матрица 4x4)

Разреженные матрицы

Для экономии памяти, если матрица содержит много нулей, используйте sp_mat:

sp_mat S(1000, 1000);  // Разреженная матрица 1000x1000
S(0, 1) = 3.14;        // Добавляем значение

Armadillo в задачах

Линейная регрессия

Линейная регрессия решается через метод наименьших квадратов:

mat X = {{1, 2}, {3, 4}, {5, 6}};  // Признаки
vec y = {7, 8, 9};                 // Целевая переменная

vec beta = solve(X.t() * X, X.t() * y);  // Решение уравнения

beta.print("Coefficients:");

K‑Means

Кластеризация с использованием K‑Means:

mat data(100, 2, fill::randu);  // 100 точек в 2D пространстве
mat centroids;

kmeans(centroids, data.t(), 3);  // Разделяем на 3 кластера
centroids.print("Cluster centroids:");

PCA

Для уменьшения размерности данных:

mat data(100, 5, fill::randu);  // 100 точек в 5-мерном пространстве

vec eigval;
mat eigvec;

princomp(eigvec, eigval, data);  // PCA

eigval.print("Eigenvalues:");
eigvec.print("Eigenvectors:");

Подробная документация доступна по ссылке.


В заключение рекомендую к посещению бесплатные открытые уроки в Otus:

  • 12 февраля: Hadoop в Docker. Подробнее

  • 27 января: SVD и ALS на службе рекомендательных систем. Подробнее

  • 30 января: Оптимизация производительности на C++. Подробнее

Полный список бесплатных уроков по Data Science и Machine Learning, а также по другим IT-направлениям можно посмотреть в календаре.

Комментарии (3)


  1. BorisU
    24.01.2025 14:07

    проверка индексов по умолчанию - сомнительное решение. Лучше наоборот, как сделано в std::vector

    C++23 на дворе, где поддержка subscript opеrator с несколькими индексами?


    1. JuPk
      24.01.2025 14:07

      До чего уж там, С++26 на подходе, там линейная алгебра из коробки будет работать. Правда, некоторые вещи из С++20 были реализованы только недавно, или полностью не реализованы до сих пор, так что от появления стандарта до его полноценной реализации проходит лет 5, не меньше. Осталось лишь лет 5 подождать, и в 2030-м 3rd-party библиотеки линейной алгебры будут не нужны)