Прискорення бібліотеки HeatonResearchNeural (нейромережі) в 30 разів

Всім привіт! Хочу поділитися невеликою історією допиливания HeatonResearchNeural — бібліотеки різноманітних нейромереж. Відразу обмовлюся, що працюю аналітиком, а чесним програмістом перестав бути років 10 тому.

Проте у мене є власний проект на C#, який розвиваю у вільний час. Щоб не морочитися написанням велосипеда колись скачав HeatonResearchNeural прикрутив скотчем і спокійно ганяв тести, допрацьовував логіку свого коду і т. д. Для максимального прискорення заклав в архітектуру рішення параллелизацию виконання розрахунків і дивлячись на завантаження CPU по 80-90% по тілу розливалося приємне хазяйське тепло — всі орють, все при справі!

З іншого боку, обсяги у мене великі, чекати доводилося довго, поки вона відпрацює. Подумував навіть про купівлю другого сервера, поки мене раптом не спало на думку зазирнути під капот цього самого велосипеда за допомогою профилировщика. Причому виникла не сама по собі, а під враженням даній статті шановного хабробщества.

Спочатку в своєму коді я був упевнений. Зрозуміло, що нейромережа це така річ, яка повинна літати, інакше не витягне серйозних тем. Однак, як після читання медичного довідника виявляєш у себе більшість ознак самих жахливих захворювань, вирішив все-таки перевіритися пройтися по коду профілювальником.

Коли профілювання вказало на бібліотечні функції, в душі затріпотіло холоднокровне хвилювання. Зрозуміло, що розробники такої гарної бібліотеки подумали про швидкість, вірно? Чи світом таки править не таємна ложа, а явна лажа? Щоб відповісти на це питання давайте розглянемо уважніше результат прогону 100 циклів моєї програми, майже цілком складається з роботи нейромереж:

image
Провалюємося в Call Tree звіту і знаходимо найважчі функції:

image
Йдемо до них і бачимо воістину несамовитий видовище. Якщо ви не зовсім впевнені у своїй психіці, можливо має сенс відвернутися і не бачити, як функція GetCol займається безбожних витягом вектора з матриці:

public Matrix GetCol(int col)
{
if (col > this.Cols)
{
throw new MatrixError("can't get column #" + col
+ " because it does not exist.");
}

double[,] newMatrix = new double[this.Rows, 1];

for (int row = 0; row < this.Rows; row++)
{
newMatrix[row, 0] = this.matrix[row, col];
}

return new Matrix(newMatrix);
}

Лише для передачі в DotProduct:

for (i = 0; i < this.next.NeuronCount; i++)
{
Matrix.Matrix col = this.matrix.GetCol(i);
double sum = MatrixMath.DotProduct(col, inputMatrix);

this.next.SetFire(i, this.activationFunction.ActivationFunction(sum));
}

Позбавляємо цього паразита поживних і повних вітамінами гігагерц двома точними ударами прямого слеша і передаємо в DotProduct відразу всю матрицю разом з потрібним номером колонки:

//Matrix.Matrix col = this.matrix.GetCol(i);
double sum = MatrixMath.DotProduct(this.matrix, i, inputMatrix);

А вже всередині замість витонченого мережива:

public static double DotProduct(Matrix a, Matrix b)
{

if (!a.IsVector() || !b.IsVector())
{
throw new MatrixError(
"To take the dot product, both matrixes must be vectors.");
}

Double[] aArray = a.ToPackedArray();
Double[] bArray = b.ToPackedArray();

if (aArray.Length != bArray.Length)

{
throw new MatrixError(
"To take the dot product, both matrixes must be of the same length.");
}

double result = 0;

int length = aArray.Length;

for (int i = 0; i < length; i++)
{
result += aArray[i] * bArray[i];
}
return result;

Ліпимо простий, як сокира, быдлокод:

public static double DotProduct(Matrix a, int i, Matrix b)
{

double result = 0;

if (!b.IsVector())
{
throw new MatrixError(
"To take the dot product, both matrixes must be vectors.");
}

if (a.Rows != b.Cols || b.Rows != 1)
{
throw new MatrixError(
"To take the dot product, both matrixes must be of the same length.");
}


int rows = a.Rows; // Так буде набагато швидше, ніж якщо вказати a.Rows прямо в умові циклу
for (int r = 0; r < rows; r++)
{
result += a[r, i] * b[0, r];
}

return result;

Валідатори на вихід за межі масиву теж має сенс закоментувати, все одно розміри задаються статично при компіляції і тут складно накосячілі, а часу на них вбивається стільки ж, скільки японців з хмарочосів при послабленні ієни на 5%.

public double this[int row, int col]
{
get
{
//Validate(row, col);
return this.matrix[row, col];
}
set
{
//Validate(row, col);
if (double.IsInfinity(value) || double.IsNaN(value))
{
throw new MatrixError("Trying to assign invalud number to matrix: "
+ value);
}
this.matrix[row, col] = value;
}
}

Отож, хвилюючий момент, запускаємо знову 100 циклів програми. Коли я побачив ефект від цих нехитрих дій, мені трохи не стало погано від радості. Таке дивне відчуття, ніби тобі ось так взяли і подарували 30 серваков (які, виявляється, стояли у тебе ж в шафі, але ти просто не здогадувався туди заглянути):

image
Перші 6 секунд на графіку це підготовка даних, тому реальний час роботи скоротилося з 780 до 26 секунд, при абсолютно однаковому результаті, природно. Таким чином, прискорення вийшло в 30 разів!

Таким чином, практика ще показує, що закони Мерфі вколюють також стабільно, як афроамериканці сидять на велфере і якщо щось може піти не так, то можна не сумніватися, що так воно і станеться. Також варто відзначити, що можливо деякі види мереж не будуть працювати на такому коді, це варто протестить і врахувати при необхідності. Усім спасибі за увагу, сподіваюся це кому-небудь хоч якось допоможе в успішній боротьбі з нестримно розростається ентропією всесвіту і коду.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.