Il codice Eigen per la moltiplicazione di matrici viene eseguito più lentamente della moltiplicazione in loop utilizzando std::vector
Sto imparando il C++ e l'apprendimento automatico, quindi ho deciso di utilizzare la libreria Eigen per la moltiplicazione di matrici. Stavo addestrando un percettrone a riconoscere una cifra dal database MNIST. Per la fase di addestramento ho impostato il numero di cicli (o epoche) di addestramento su T = 100.
La "matrice di addestramento" è una matrice 10000 x 785. L'elemento zero di ogni riga contiene l'"etichetta" che identifica la cifra a cui sono mappati i dati di input (i restanti 784 elementi della riga).
Esiste anche un vettore "pesi" 784 x 1 che contiene i pesi per ciascuna delle 784 caratteristiche. Il vettore dei pesi verrebbe moltiplicato per ciascun vettore di input (una riga della matrice di addestramento escludendo l'elemento zeroth) e verrebbe aggiornato ogni iterazione, e ciò accadrebbe T volte per ciascuno dei 10000 input.
Ho scritto il seguente programma (che cattura l'essenza di ciò che sto facendo), dove ho confrontato l'approccio "vanilla" di moltiplicare le righe di una matrice con il vettore di peso (usando std::vector e loop) con quello che sentivo fosse il meglio che potevo fare con un approccio Eigen. Non è davvero una moltiplicazione di una matrice con un vettore, in realtà sto tagliando la riga della matrice di allenamento e moltiplicandola con il vettore del peso.
La durata del periodo di addestramento per l'approccio std::vector era di 160,662 ms e per il metodo Eigen era solitamente superiore a 10.000 ms.
Compilo il programma usando il seguente comando:
clang++ -Wall -Wextra -pedantic -O3 -march=native -Xpreprocessor -fopenmp permute.cc -o perm -std=c++17
Sto usando un MacBook Pro "metà" del 2012 con macOS Catalina e con i5 dual core da 2,5 GHz.
#include <iostream>
#include <algorithm>
#include <random>
#include <Eigen/Dense>
#include <ctime>
#include <chrono>
using namespace Eigen;
int main() {
Matrix<uint8_t, Dynamic, Dynamic> m = Matrix<uint8_t, Dynamic, Dynamic>::Random(10000, 785);
Matrix<double, 784, 1> weights_m = Matrix<double, 784, 1>::Random(784, 1);
Matrix<uint8_t, 10000, 1> y_m, t_m;
std::minstd_rand rng;
rng.seed(time(NULL));
std::uniform_int_distribution<> dist(0,1); //random integers between 0 and 1
for (int i = 0; i < y_m.rows(); i++) {
y_m(i) = dist(rng);
t_m(i) = dist(rng);
}
int T = 100;
int err;
double eta;
eta = 0.25; //learning rate
Matrix<double, 1, 1> sum_wx_m;
auto start1 = std::chrono::steady_clock::now(); //start of Eigen Matrix loop
for (int iter = 0; iter < T; iter++) {
for (int i = 0; i < m.rows(); i++) {
sum_wx_m = m.block(i, 1, 1, 784).cast<double>() * weights_m;
//some code to update y_m(i) based on the value of sum_wx_m which I left out
err = y_m(i) - t_m(i);
if (fabs(err) > 0) { //update the weights_m matrix if there's a difference between target and predicted
weights_m = weights_m - eta * err * m.block(i, 1, 1, 784).transpose().cast<double>();
}
}
}
auto end1 = std::chrono::steady_clock::now();
auto diff1 = end1 - start1;
std::cout << "Eigen matrix time is "<<std::chrono::duration <double, std::milli> (diff1).count() << " ms" << std::endl;
//checking how std::vector form performs;
std::vector<std::vector<uint8_t>> v(10000);
std::vector<double> weights_v(784);
std::vector<uint8_t> y_v(10000), t_v(10000);
for (unsigned long i = 0; i < v.size(); i++) {
for (int j = 0; j < m.cols(); j++) {
v[i].push_back(m(i, j));
}
}
for (unsigned long i = 0; i < weights_v.size(); i++) {
weights_v[i] = weights_m(i);
}
for (unsigned long i = 0; i < y_v.size(); i++) {
y_v[i] = dist(rng);
t_v[i] = dist(rng);
}
double sum_wx_v;
auto start2 = std::chrono::steady_clock::now(); //start of vector loop
for (int iter = 0; iter < T; iter++) {
for(unsigned long j = 0; j < v.size(); j++) {
sum_wx_v = 0.0;
for (unsigned long k = 1; k < v[0].size() ; k++) {
sum_wx_v += weights_v[k - 1] * v[j][k];
}
//some code to update y_v[i] based on the value of sum_wx_v which I left out
err = y_v[j] - t_v[j];
if (fabs(err) > 0) {//update the weights_v matrix if there's a difference between target and predicted
for (unsigned long k = 1; k < v[0].size(); k++) {
weights_v[k - 1] -= eta * err * v[j][k];
}
}
}
}
auto end2 = std::chrono::steady_clock::now();
auto diff2 = end2 - start2;
std::cout << "std::vector time is "<<std::chrono::duration <double, std::milli> (diff2).count() << " ms" << std::endl;
}
Quali modifiche devo apportare per ottenere tempi di esecuzione migliori?
Risposte
Potrebbe non essere la soluzione migliore, ma puoi provare:
- Poiché l'ordine dei dati predefinito di Eigen è Column-Major, puoi lasciare che la tua matrice di addestramento sia 785x10000 in modo tale che ogni coppia etichetta/dati di addestramento sia contigua nella memoria (cambia anche la riga in cui viene calcolata sum_wx_m).
- Usa la versione a dimensione fissa delle operazioni di blocco, ovvero puoi sostituire m.block(i, 1, 1, 784) con m.block<1,784>(i, 1) (in ordine inverso se hai già cambiato la matrice di addestramento layout oppure puoi semplicemente mappare la parte dei dati della tua matrice di addestramento e utilizzare il riferimento .col() [vedi l'esempio sotto])
Ecco il tuo codice modificato sulla base di queste idee:
#include <iostream>
#include <algorithm>
#include <random>
#include <Eigen/Dense>
#include <ctime>
#include <chrono>
using namespace Eigen;
int main() {
Matrix<uint8_t, Dynamic, Dynamic> m = Matrix<uint8_t, Dynamic, Dynamic>::Random(785, 10000);
Map<Matrix<uint8_t, Dynamic, Dynamic>> m_data(m.data() + 785, 784, 10000);
Matrix<double, 784, 1> weights_m = Matrix<double, 784, 1>::Random(784, 1);
Matrix<uint8_t, 10000, 1> y_m, t_m;
std::minstd_rand rng;
rng.seed(time(NULL));
std::uniform_int_distribution<> dist(0,1); //random integers between 0 and 1
for (int i = 0; i < y_m.rows(); i++) {
y_m(i) = dist(rng);
t_m(i) = dist(rng);
}
int T = 100;
int err;
double eta;
eta = 0.25; //learning rate
Matrix<double, 1, 1> sum_wx_m;
auto start1 = std::chrono::steady_clock::now(); //start of Eigen Matrix loop
for (int iter = 0; iter < T; iter++) {
for (int i = 0; i < m.cols(); i++) {
sum_wx_m = weights_m.transpose() * m_data.col(i).cast<double>();
//some code to update y_m(i) based on the value of sum_wx_m which I left out
err = y_m(i) - t_m(i);
if (fabs(err) > 0) { //update the weights_m matrix if there's a difference between target and predicted
weights_m = weights_m - eta * err * m_data.col(i).cast<double>();
}
}
}
auto end1 = std::chrono::steady_clock::now();
auto diff1 = end1 - start1;
std::cout << "Eigen matrix time is "<<std::chrono::duration <double, std::milli> (diff1).count() << " ms" << std::endl;
//checking how std::vector form performs;
std::vector<std::vector<uint8_t>> v(10000);
std::vector<double> weights_v(784);
std::vector<uint8_t> y_v(10000), t_v(10000);
for (unsigned long i = 0; i < v.size(); i++) {
for (int j = 0; j < m.rows(); j++) {
v[i].push_back(m(j, i));
}
}
for (unsigned long i = 0; i < weights_v.size(); i++) {
weights_v[i] = weights_m(i);
}
for (unsigned long i = 0; i < y_v.size(); i++) {
y_v[i] = dist(rng);
t_v[i] = dist(rng);
}
double sum_wx_v;
auto start2 = std::chrono::steady_clock::now(); //start of vector loop
for (int iter = 0; iter < T; iter++) {
for(unsigned long j = 0; j < v.size(); j++) {
sum_wx_v = 0.0;
for (unsigned long k = 1; k < v[0].size() ; k++) {
sum_wx_v += weights_v[k - 1] * v[j][k];
}
//some code to update y_v[i] based on the value of sum_wx_v which I left out
err = y_v[j] - t_v[j];
if (fabs(err) > 0) {//update the weights_v matrix if there's a difference between target and predicted
for (unsigned long k = 1; k < v[0].size(); k++) {
weights_v[k - 1] -= eta * err * v[j][k];
}
}
}
}
auto end2 = std::chrono::steady_clock::now();
auto diff2 = end2 - start2;
std::cout << "std::vector time is "<<std::chrono::duration <double, std::milli> (diff2).count() << " ms" << std::endl;
}
Ho compilato questo codice nel mio Ubuntu Desktop con i7-9700K:
g++ -Wall -Wextra -O3 -std=c++17
====================================
Eigen matrix time is 110.523 ms
std::vector time is 117.826 ms
g++ -Wall -Wextra -O3 -march=native -std=c++17
=============================================
Eigen matrix time is 66.3044 ms
std::vector time is 71.2296 ms
Dopo aver discusso con gli utenti J. Schultke e puhu, ho apportato le seguenti modifiche al mio codice:
- Ho cambiato tutte le chiamate m.block(i, 1, 1, 784) in m.block<1, 784>(i, 1) , questo riduce di un terzo il tempo richiesto per il loop della matrice Eigen . (suggerito per la prima volta da J. Schultke)
- Ho dichiarato la mia matrice m come memorizzata nell'ordine RowMajor . Questo perché per impostazione predefinita le matrici Eigen sono memorizzate nell'ordine ColMajor (colonna-maggiore). Ciò farebbe sì che ogni voce di una riga venga memorizzata in modo contiguo. Quindi ora le chiamate m.block() , che uso per fare riferimento a una fetta di riga nella matrice m, recupereranno semplicemente l'intero blocco di memoria in una volta, riducendo il tempo "Matrice Eigen" al di sotto di "std: :vettore" tempo. (suggerito da puhu)
I tempi di esecuzione medi ora sono
cpp:Pro$ ./perm
Eigen matrix time is 134.76 ms
std::vector time is 155.574 ms
e il codice modificato è:
#include <iostream>
#include <algorithm>
#include <random>
#include <Eigen/Dense>
#include <chrono>
#include <ctime>
using namespace Eigen;
int main() {
Matrix<uint8_t, Dynamic, Dynamic, RowMajor> m = Matrix<uint8_t, Dynamic, Dynamic, RowMajor>::Random(10000, 785);
Matrix<double, 784, 1> weights_m = Matrix<double, 784, 1>::Random(784, 1);
Matrix<uint8_t, 10000, 1> y_m, t_m;
std::minstd_rand rng;
rng.seed(time(NULL));
std::uniform_int_distribution<> dist(0,1); //random integers between 0 and 1
for (int i = 0; i < y_m.rows(); i++) {
y_m(i) = dist(rng);
t_m(i) = dist(rng);
}
int T = 100;
int err;
double eta;
eta = 0.25; //learning rate
Matrix<double, 1, 1> sum_wx_m;
auto start1 = std::chrono::steady_clock::now(); //start of Eigen Matrix loop
for (int iter = 0; iter < T; iter++) {
for (int i = 0; i < m.rows(); i++) {
auto b = m.block<1, 784>(i, 1).cast<double>();
sum_wx_m = b * weights_m;
//some code to update y_m(i) based on the value of sum_wx_m which I left out
err = y_m(i) - t_m(i);
if (fabs(err) > 0) { //update the weights_m matrix if there's a difference between target and predicted
weights_m = weights_m - eta * err * b.transpose();
}
}
}
auto end1 = std::chrono::steady_clock::now();
auto diff1 = end1 - start1;
std::cout << "Eigen matrix time is "<<std::chrono::duration <double, std::milli> (diff1).count() << " ms" << std::endl;
//checking how std::vector form performs;
std::vector<std::vector<uint8_t>> v(10000);
std::vector<double> weights_v(784);
std::vector<uint8_t> y_v(10000), t_v(10000);
for (unsigned long i = 0; i < v.size(); i++) {
for (int j = 0; j < m.cols(); j++) {
v[i].push_back(m(i, j));
}
}
for (unsigned long i = 0; i < weights_v.size(); i++) {
weights_v[i] = weights_m(i);
}
for (unsigned long i = 0; i < y_v.size(); i++) {
y_v[i] = dist(rng);
t_v[i] = dist(rng);
}
double sum_wx_v;
auto start2 = std::chrono::steady_clock::now(); //start of vector loop
for (int iter = 0; iter < T; iter++) {
for(unsigned long j = 0; j < v.size(); j++) {
sum_wx_v = 0.0;
for (unsigned long k = 1; k < v[0].size() ; k++) {
sum_wx_v += weights_v[k - 1] * v[j][k];
}
//some code to update y_v[i] based on the value of sum_wx_v which I left out
err = y_v[j] - t_v[j];
if (fabs(err) > 0) {//update the weights_v matrix if there's a difference between target and predicted
for (unsigned long k = 1; k < v[0].size(); k++) {
weights_v[k - 1] -= eta * err * v[j][k];
}
}
}
}
auto end2 = std::chrono::steady_clock::now();
auto diff2 = end2 - start2;
std::cout << "std::vector time is "<<std::chrono::duration <double, std::milli> (diff2).count() << " ms" << std::endl;
}