Une introduction à Armadillo

Armadillo est une librarie template C++ avec une interface très complete pour le calcul matriciel et l’algèbre linéaire. Elle permet de rapidement obtenir un code fonctionel sans compromis sur la performance du language.

Ce tutoriel couvre l’installation de cette librarie (sur macOS seulement pour le moment), l’execution de simple code et un tour de sa riche interface. Une alternative à Armadillo est Eigen.


Armadillo est developpé et maintenu par la NITCA (National Information and Communications Technology of Australia). Leur but était de fournir une interface aussi facile d’utilisation que celle de Matlab pour les utilisateurs du language de bas-niveau qu’est C++. Nombreuses sont les librairies qui dépendent de Armadillo comme MLPACK (pour l’apprentissage statistique) et SigPack (pour le traitement du signal).

Armadillo utilise BLAS et LAPACK; BLAS est un ensemble d’operations de bases d’albgèbre linéaire écrites en Fortran et est à la base de nombreuses libraries de calcul matriciel tel que Numpy et de languages comme Matlab. LAPACK fut lancée en 1992 et est une librarie Fortran offrant des solutions à une variété de problèmes d’analyse numérique.

Armadillo est couvert sous la licence Apache 2.0, donc libre au lecteur de l’utiliser dans ses projets.


Installation

Je recommande d’installer Armadillo avec le gestionnaire de paquets de macOS Homebrew. C’est l’un des gestionnaires les plus utilisés pour le système d’exploitation de Apple du fait de sa facilité d’utilisation et de son interface integrée dans le terminal. Si Homebrew n’est pas encore installé dans votre système, copiez cette commande dans votre terminal:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Maintenant que Hombrew est installé nous pouvons maintenant installer Armadillo:

$ brew install armadillo --c++11

Une autre methode pour installer Armadillo est d’utiliser un autre gestionnaire CMake qui est cependant plus difficile d’utilisation et qui permet une installation ‘sur-mesure’ que nous ne couvrons pas ici.

Compilation

Pour utiliser Armadillo dans un fichier C++ il faut d’abord importer la librairie:

#include <armadillo>

Pour compiler et executer du code C++ depuis le terminal, il faut d’abord se déplacer dans le dossier où se situe ce ficher (en utilisant la commande ‘cd’) et ensuite préciser au compilateur (g++ a été utiliser pour développer ce tutorial) où se trouve la librairie lorsque l’on compile le fichier source avec la commande:

$ g++ main.cc -o main.o -O2 -larmadillo

Pour utiliser la librairie dans Xcode, il suffit d’ajouter à la liste des ‘Other C++ Flags’:

-l/usr/local/opt/armadillo/include

Premiers pas

Armadillo permet la manipulation de tableaux de 1, 2 ou 3 dimensions et définit de nombreux opérateurs classiques ce qui permet d’écrire un code compact et lisible. Nous nous limitons ici aux tableaux de 1 ou 2 dimensions.

Les classes de tableaux sont les suivantes:

  • matrices (MxN);
  • vecteurs; qui peuventêtre définis comme ligne (Nx1) ou colonne(1xN) et sont des sous-classes de matrices;

Les classes de la librarie sont écrites utilisant le principe du polymorphisme statique. Ainsi pour créer une matrice par example il faut préciser au compilateur le type de donnée que notre matrice va contenir; par exemple pour créer une matrice de doubles, il faut procéder comme suit:

arma::Mat<double> A = arma::Mat<double>(); 
arma::mat B = arma::mat(); // both are equivalent

Afin d’éviter d’avoir à utiliser le namespace “arma” il faut juste ajouter la ligne de code:

using namespace arma;

Des types classiques ont été créées par convénience; pour les doubles par exemple:

arma::mat A = arma::mat(); // matrix of doubles
arma::vec A = arma::vec(); vector of doubles

pour créer des tableaux d’entier naturels positifs:

arma::umat A = arma::mat(); // matrix of unsigned int
arma::uvec A = arma::vec(); vector of unsigned int

Il y a plusieurs types de constructeurs disponibles pour créer des tableaux données. Je recommande personnellement d’éviter les constructeurs par défauts et au lieu d’utiliser le constructeur permettant de définir la valeur de tous les elements à zero:

Le code suivant:

arma::mat M1 = arma::mat(5,10,arma::fill::zero);
arma::mat M2 = arma::mat(size(M1),arma::fill::randu);
arma::mat M3 = arma::mat(size(M1),arma::fill::ones);
std::cout << M1.n_rows << "\n"; // returns '5'
std::cout << M1.n_cols << "\n"; // returns '10'
std::cout << M1.n_nonzero << "\n"; // returns '0'
std::cout << M3.n_nonzero << "\n"; // returns '50'

crée une matrice “M1” avec 5 lignes et 10 colonnes dont les éléments sont égaux à zéro, une matrice “M2” de la même taille que “M1” dont les éléments sont des réalisations indépendantes de la loi uniforme sur l’intervalle [0,1] et enfin une matrice “M3” de même taille que “M1” dont tous les éléments sont égaux à 1. Les options de “remplissage” sont les suivantes:

  • arma::fill::zero: initialise tous les éléments à 0;
  • arma::fill::ones: initialise tous les éléments à 1;
  • arma::fill::eye: initialise les éléments de la diagonale à 1 et les autres éléments à 0;
  • arma::fill::randu: initialise les éléments comme réalisations indépendantes de la loi uniforme sur l’intervalle [0,1];
  • arma::fill::randn: initialise les éléments comme réalisations indépendantes de la loi normale centrée réduite;
  • arma::fill::none: ne modifie aucun élément;

Une méthode assez conveniente pour créer une matrice est à partir d’une chaîne de caractères. Chaque élément doit être séparé par un espace et chaque ligne par un point-virgule:

arma::mat fromString arma::mat("1 9;-1 3");

Si le format n’est pas respecté alors une erreur fera arrêter la compilation.

Armadillo traite les scalaires comme des matrices de taille (1,1) ou des vecteurs de taille 1; par ailleurs certaines constantes utiles sont définies dans la librairie:

arma::datum::pi; // pi
arma::datum::inf; // infinite
arma::datum::nan; // not a number

Accéder aux éléments

Les éléments des matrices peuvent être accédés utilisant l’indexation par numéro de ligne et/ou colonne grâce aux opérateurs (), [] ou de la fonction .at().

L’opérateur recommandé en phase de développement est () qui est moins performant mais qui vérifie que l’indice demandé est à l’intérieur des bornes de la matrice en question, sinon une exception est signalée par le compilateur. Ce n’est pas le cas des opérateurs [] ou .at() qui ne signaleront rien d’où l’importance de ne les utiliser que lorsque le code est testé puisqu’ils amélioront la performance.

fromString(0,0); // extrait '1'
fromString(30,30); // erreur !
fromString[1,0]; // extrait '9'
fromString[10,0]; // ??

Il est aussi possible d’extraire une ligne/colonne donnée d’une matrice en utilisant les fonctions membres .col() et .row():

vec x = vec();
colvec y = colvec();
x = fromString.row(0);  // x = [1,9] 
y = fromString.col(0); // x = [1;-1]

Une méthode plus avancée est l’extraction de sous-matrices grâce à la méthode .submat(); cette opérateur fournit un accées lecture/écritude de la sous-matrice ce qui est particulièrement intéressant pour modifier des parties choisies d’une matrice:

int firstR(2),lastR(3),firstC(3),lastC(4);
// -- 1 --
arma::mat X = arma::mat(10,10,arma::fill::ones);
arma::mat Y = X.submat(firstR,lastR,firstC,lastC);
std::cout << Y.n_rows << "\n"; 
std::cout << Y.n_cols << "\n";
// -- 2 -- 
X.submat(firstR,lastR,firstC,lastC) = mat(size(Y),arma::fill::zeros)

La première partie de ces quelques lignes de code permet de crée une matrice carrée de taille 10 “X” dont ls éléments sont tous égaux à 1, ensuite crée une matrice “Y” qui est une sous-matrice de taille 2 “X” (ligne 2 à ligne 3 et colonne 3 à colonne 4) et ensuite affiche le nombre de lignes et le nombre de colonnes de “Y”;

La deuxième partie du code modifie la sous-matrice de “X”, plus précisément definit X(2,3), X(2,4), X(3,3) et X(3,4) tous égaux à 0.

Vérifier les propriétés de matrices

  • in_range() vérifie qu’un indice est valide, c’est-à-dire que si on essaye d’y accéder avec l’opérateur () une exception ne sera pas lancée.
arma::mat X = arma::mat(4,4,arma::fill:randu);
std::cout << X.in_range(2,3) << "\n"; // returns True
std::cout << X.in_range(1,4) << "\n"; // returns False
std::cout << X.in_range(5,0) << "\n"; // returns False
std::cout << X.in_range(10,100) << "\n"; // returns False
  • is_empty() vérifie qu’une matrice est “vide”. Cette function membre renvoie la valeur True pour les matrices pour lesquelles la méthode .reset() a été appliquée ou pour les éléments initialisés avec le constructeur par défaut.
arma::mat X = arma::mat();
std::cout << X.is_empty() << "\n"; returns True;
X(0,0) = double(0.5);
std::cout << X.is_empty() << "\n"; returns False;
X.reset();
std::cout << X.is_empty() << "\n"; returns True;
  • is_finite() vérifie que tous les éléments d’une matrice sont finis (i.e. différent de inf ou de nan)
arma::mat X = arma::mat(3,10,arma::fill::ones);
std::cout << X.is_finite() << "\n"; returns True;
X(1,1) = arma::datum::inf;
std::cout << X.is_finite() << "\n"; returns False;
X(1,1) = double(0.0);
X(1,1) = arma::datum::inf;
std::cout << X.is_finite() << "\n"; returns False;
  • is_square() vérifie que la matrice est carrée (nombre de lignes égal au nombre de colonnes):
arma::mat X = arma::mat(3,10,arma::fill::ones);
std::cout << X.is_square() << "\n"; returns False;
arma::mat Y = arma::mat(3,3,arma::fill::eye);
std::cout << Y.is_square() << "\n"; returns True;
  • is_vec() vérifie si l’un des composants de la taille de la matrice est égal à 1 (i.e. s’il s’agit d’un vecteur ligne ou colone);
arma::mat a = arma::mat(3,10,arma::fill::ones);
std::cout << a.is_vec() << "\n"; returns False;
arma::mat b = arma::mat(1,3,arma::fill::zeros);
std::cout << b.is_vec() << "\n"; returns True;
arma::mat c = arma::mat(3,1,arma::fill::randu);
std::cout << c.is_vec() << "\n"; returns True;
arma::vec d = arma::vec(4,arma::fill::randu);
std::cout << d.is_vec() << "\n"; returns True;
arma::colvec e = arma::colvec(3,arma::fill::randu);
std::cout << e.is_vec() << "\n"; returns True;
  • has_inf() vérifie que les éléments de la matrice sont tous finis (i.e. différents de inf):
arma::vec r = arma::vec(100,arma::fill::zero);
std::cout << r.has_inf() << "\n"; returns False;
r(99) = arma::datum::nan;
std::cout << r.has_inf() << "\n"; returns False;
r(99) = arma::datum::inf;
std::cout << r.has_inf() << "\n"; returns True;
  • has_nan() vérifie que les éléments de la matrice sont tous des nombres(i.e. différents de nan):
arma::vec r = arma::vec(100,arma::fill::zero);
std::cout << r.has_nan() << "\n"; returns False;
r(99) = arma::datum::inf;
std::cout << r.has_nan() << "\n"; returns False;
r(99) = arma::datum::nan;
std::cout << r.has_nan() << "\n"; returns True;

Calcul matriciel

Armadillo redéfinit les opérateurs suivants :

  • q * A ou A * q : multiplication des éléments d’une matrice A par un scalaire q;
  • A + B : addition élément par éléments d’une matrice (elles doivent avoir la même taille);
  • A * B : multiplication classique de matrices (les tailles doivent être compatibles);
  • A / B : division élément par élément de la matrice A par la matrice B (elles doivent être de même taille et B doit être une matrice dont tous les éléments sonts différents de zéro);
  • A == B : revoie la valeur True si et seulement si les matrices sont de même taille et les éléments à la même position ont la même valeur;
  • A != B : revoie la valeur True si et seulement siA == B renvoie la valeur False;
  • A % B : multiplicatio élément par élément de la matrice A par la matrice B (elles doivent être de même taille);

Dans les prochaines publications nous nous concentrons l’accès d’éléments avec des méthodes plus avancées, les fonctions d’analyse numérique de la librarie et enfin les tableaux à 3 dimensions.