Привет, Хабр!
Работа с матрицами в стандартном C++ — это боль, страдание и масса ненужного кода. Почему? Потому что стандартная библиотека STL вообще не предназначена для линейной алгебры. Например, если вы хотите умножить две матрицы, вам придётся писать кастомный алгоритм, отлаживать его, а затем ещё раз писать его, когда поймёте, что оптимизация не та.
Но есть библиотека Armadillo — и с ней всё иначе. Вот её главные фичи:
Простота: вы работаете с матрицами, как с обычными переменными. Никаких «джунглей» из циклов.
Скорость: поддержка OpenBLAS, MKL и LAPACK делает вычисления молниеносными.
Возможности: встроенные методы для кластеризации, PCA, разреженных матриц и даже машинного обучения.
Универсальность: можно работать как с плотными, так и с разреженными данными.
Теперь перейдём синтаксису.
Базовый синтаксис 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-направлениям можно посмотреть в календаре.
BorisU
проверка индексов по умолчанию - сомнительное решение. Лучше наоборот, как сделано в std::vector
C++23 на дворе, где поддержка subscript opеrator с несколькими индексами?
JuPk
До чего уж там, С++26 на подходе, там линейная алгебра из коробки будет работать. Правда, некоторые вещи из С++20 были реализованы только недавно, или полностью не реализованы до сих пор, так что от появления стандарта до его полноценной реализации проходит лет 5, не меньше. Осталось лишь лет 5 подождать, и в 2030-м 3rd-party библиотеки линейной алгебры будут не нужны)