Сверточный Слой: Быстрая Свертка С Использованием Метода Шмуэля Винограда



Введение Эта статья является продолжением серия статей описание алгоритмов, лежащих в основе Синет — фреймворк для запуска предварительно обученных нейронных сетей на ЦП.

В предыдущем статья Я описал методы, основанные на умножении матриц.

Эти методы при минимальных усилиях позволяют достичь во многих случаях более 80% теоретического максимума.

Казалось бы, где можно еще улучшиться? Оказывается, это возможно! Существуют математические методы, позволяющие сократить количество операций, необходимых для свертки.

В этой статье мы рассмотрим один из этих методов — алгоритм свертки с использованием метода Винограда.



Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Шмуэль Виноград 1936.01.04 - 2019.03.25 — выдающийся израильский и американский учёный-компьютерщик, создатель алгоритмов быстрого умножения матриц, свертки и преобразования Фурье.



Немного математики

Хотя в машинном обучении чаще всего используются свертки с квадратным ядром, для упрощения изложения сначала рассмотрим одномерный случай.

Пусть у нас есть входное одномерное изображение размера

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

и размер фильтра

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

тогда результат свертки будет:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Эти выражения будет удобно переписать в матричной форме:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

где используются обозначения:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

На первый взгляд последняя замена выглядит несколько бессмысленной — операций явно больше.

Но выражения

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

И

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

необходимо рассчитать только один раз.

С учетом этого нам нужно выполнить всего 4 операции умножения, тогда как в исходной формуле их нужно сделать 6. Перепишем выражение (1) еще раз:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Где

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

обозначает поэлементное умножение, также используются следующие обозначения:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Выражение (2) можно обобщить на двумерный случай:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Необходимое количество операций умножения для

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

будет сокращено с

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

до

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

Таким образом, мы получаем вычислительный выигрыш в

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

раз.

Если визуализировать, то по сути вместо того, чтобы отдельно рассчитывать свертку для каждой точки изображения, мы будем вычислять ее сразу для блока размером

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

В произвольном случае для свертки с окном

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

и размер блока

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

необходимое количество умножений будет

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

а выигрыш описывается формулой:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

В пределе для достаточно больших

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

И

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Для любой свертки достаточно всего 1 операции умножения на точку! К сожалению, увеличение размера блока приводит к быстрому увеличению затрат на преобразование входных данных.



Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

и выходной

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

изображений, поэтому на практике вы обычно не используете размер блока больше, чем

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.



Пример реализации

Для практической реализации алгоритма Винограда хотелось бы рассмотреть простейший случай — свертку с ядром.



Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

для блока

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

Чтобы еще больше упростить изложение, мы также предположим, что во входном изображении нет отступов, а размеры входного и выходного изображений кратны 2. Реализация свертки, основанной на умножении матриц, будет выглядеть так:

  
  
  
  
  
   

float relu(float value) { return value > 0 ? return value : 0; } void gemm_nn(int M, int N, int K, float alpha, const float * A, int lda, float beta, const float * B, int ldb, float * C, int ldc) { for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) { C[i*ldc + j] = beta; for (int k = 0; k < K; ++k) C[i*ldc + j] += alpha * A[i*lda + k] * B[k*ldb + j]; } } void im2col(const float * src, int srcC, int srcH, int srcW, int kernelY, int kernelX, float * buf) { int dstH = srcH - kernelY + 1; int dstW = srcW - kernelX + 1; for (int sc = 0; sc < srcC; ++sc) for (int ky = 0; ky < kernelY; ++ky) for (int kx = 0; kx < kernelX; ++kx) for (int dy = 0; dy < dstH; ++dy) for (int dx = 0; dx < dstW; ++dx) *buf++ = src[((b * srcC + sc)*srcH + dy + ky)*srcW + dx + kx]; } void convolution(const float * src, int batch, int srcC, int srcH, int srcW, int kernelY, int kernelX, int dilationY, int dilationX, int strideY, int strideX, int padY, int padX, int padH, int padW, int group, const float * weight, const float * bias, float * dst, int dstC, float * buf) { int dstH = srcH - kernelY + 1; int dstW = srcW - kernelX + 1; int M = dstC; int N = dstH * dstW; int K = srcC * kernelY * kernelX; for (int b = 0; b < batch; ++b) { im2col(src, srcC, srcH, srcW, kernelY, kernelX, buf); gemm_nn(M, N, K, 1, weight, K, 0, buf, N, dst, N)); for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) dst[i*N+ j] = relu(dst[i*N + j] + bias[i]); src += srcC*srcH*srcW; dst += dstC*dstH*dstW; } }

Тем, кто хочет подробно разобраться в том, что здесь происходит, следует обратиться к моему предыдущая статья .

Текущая реализация получается из предыдущей, если положить:

strideY = strideX = dilationY = dilationX = group = 1, padY = padX = padH = padW = 0.



Модифицированный алгоритм свертки

Позвольте мне еще раз процитировать формулу (6):

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

Если мы перейдем от выходного изображения размера

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

к изображению произвольного размера, то нам придется разбить его на блоки размера

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

Для каждого такого блока

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

значения, которые входят в 16 преобразованных

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

входные изображения уменьшаются вдвое.

Каждая из этих 16 матриц затем умножается на соответствующую матрицу преобразованных весов.



Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

Полученные 16 матриц преобразуются в выходное изображение.



Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

Схема этого процесса представлена ниже:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

С учетом этих замечаний алгоритм свертки примет вид:

void convolution(const float * src, int batch, int srcC, int srcH, int srcW, int kernelY, int kernelX, int dilationY, int dilationX, int strideY, int strideX, int padY, int padX, int padH, int padW, int group, const float * weight, const float * bias, float * dst, int dstC, float * buf) { const int block = 2, kernel = 3; int count = (block + kernel - 1)*(block + kernel - 1); // 16 int dstH = srcH - kernelY + 1; int dstW = srcW - kernelX + 1; int tileH = dstH / block; int tileW = dstW / block; int sizeW = srcC * dstC; int sizeS = srcC * tileH * tileW; int sizeD = dstC * tileH * tileW; int M = dstC; int N = tileH * tileW; int K = srcC; float * bufW = buf; float * bufS = bufW + sizeW*count; float * bufD = bufS + sizeS*count; set_filter(weight, sizeW, bufW); for (int b = 0; b < batch; ++b) { set_input(src, srcC, srcH, srcW, bufS, sizeS); for (int i = 0; i < count; ++i) gemm_nn(M, N, K, 1, bufW + i * sizeW, K, bufS + i * sizeS, N, 0, bufD + i * sizeD, N)); set_output(bufD, sizeD, dst, dstC, dstH, dstW); for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) dst[i*N+ j] = relu(dst[i*N + j] + bias[i]); src += srcC*srcH*srcW; dst += dstC*dstH*dstW; } }

Количество операций, которые потребуются без учета преобразований входного и выходного изображений: ~srcC*dstC*dstH*dstW*count .

Далее мы опишем алгоритмы преобразования весов, входных и выходных изображений.



Преобразование сверточных весов

Как видно из выражения (6), над весами свертки необходимо выполнить следующее преобразование:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

, где матрица

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

определяется в выражении (4).

Код такого преобразования будет выглядеть так:

void set_filter(const float * src, int size, float * dst) { for (int i = 0; i < size; i += 1, src += 9, dst += 1) { dst[0 * size] = src[0]; dst[1 * size] = (src[0] + src[2] + src[1])/2; dst[2 * size] = (src[0] + src[2] - src[1])/2; dst[3 * size] = src[2]; dst[4 * size] = (src[0] + src[6] + src[3])/2; dst[5 * size] = ((src[0] + src[6] + src[3]) + (src[2] + src[8] + src[5]) + (src[1] + src[7] + src[4]))/4; dst[6 * size] = ((src[0] + src[6] + src[3]) + (src[2] + src[8] + src[5]) - (src[1] + src[7] + src[4]))/4; dst[7 * size] = (src[2] + src[8] + src[5])/2; dst[8 * size] = (src[0] + src[6] - src[3])/2; dst[9 * size] = ((src[0] + src[6] - src[3]) + (src[2] + src[8] - src[5]) + (src[1] + src[7] - src[4]))/4; dst[10 * size] = ((src[0] + src[6] - src[3]) + (src[2] + src[8] - src[5]) - (src[1] + src[7] - src[4]))/4; dst[11 * size] = (src[2] + src[8] - src[5])/2; dst[12 * size] = src[6]; dst[13 * size] = (src[6] + src[8] + src[7])/2; dst[14 * size] = (src[6] + src[8] - src[7])/2; dst[15 * size] = src[8]; } }

К счастью, это преобразование нужно выполнить только один раз.

Поэтому на конечную производительность это не влияет.

Преобразование входного изображения

Как видно из выражения (6), над входным изображением необходимо выполнить следующее преобразование:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

, где матрица

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

определяется в выражении (3).

Код такого преобразования будет выглядеть так:

void set_input(const float * src, int srcC, int srcH, int srcW, float * dst, int size) { int dstH = srcH - 2; int dstW = srcW - 2; for (int c = 0; c < srcC; ++c) { for (int row = 0; row < dstH; row += 2) { for (int col = 0; col < dstW; col += 2) { float tmp[16]; for (int r = 0; r < 4; ++r) for (int c = c; c < 4; ++c) tmp[r * 4 + c] = src[(row + r) * srcW + col + c]; dst[0 * size] = (tmp[0] - tmp[8]) - (tmp[2] - tmp[10]); dst[1 * size] = (tmp[1] - tmp[9]) + (tmp[2] - tmp[10]); dst[2 * size] = (tmp[2] - tmp[10]) - (tmp[1] - tmp[9]); dst[3 * size] = (tmp[1] - tmp[9]) - (tmp[3] - tmp[11]); dst[4 * size] = (tmp[4] + tmp[8]) - (tmp[6] + tmp[10]); dst[5 * size] = (tmp[5] + tmp[9]) + (tmp[6] + tmp[10]); dst[6 * size] = (tmp[6] + tmp[10]) - (tmp[5] + tmp[9]); dst[7 * size] = (tmp[5] + tmp[9]) - (tmp[7] + tmp[11]); dst[8 * size] = (tmp[8] - tmp[4]) - (tmp[10] - tmp[6]); dst[9 * size] = (tmp[9] - tmp[5]) + (tmp[10] - tmp[6]); dst[10 * size] = (tmp[10] - tmp[6]) - (tmp[9] - tmp[5]); dst[11 * size] = (tmp[9] - tmp[5]) - (tmp[11] - tmp[7]); dst[12 * size] = (tmp[4] - tmp[12]) - (tmp[6] - tmp[14]); dst[13 * size] = (tmp[5] - tmp[13]) + (tmp[6] - tmp[14]); dst[14 * size] = (tmp[6] - tmp[14]) - (tmp[5] - tmp[13]); dst[15 * size] = (tmp[5] - tmp[13]) - (tmp[7] - tmp[15]); dst++; } } src += srcW * srcH; } }

Количество операций, необходимых для этого преобразования: ~srcH*srcW*srcC*count .



Преобразование выходного изображения

Как видно из выражения (6), над выходным изображением необходимо выполнить следующее преобразование:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

, где матрица

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

определяется в выражении (5).

Код такого преобразования будет выглядеть так:

void set_output(const float * src, int size, float * dst, int dstC, int dstH, int dstW) { for (int c = 0; c < dstC; ++c) { for (int row = 0; row < dstH; row += 2) { for (int col = 0; col < dstW; col += 2) { float tmp[8]; tmp[0] = src[0 * size] + src[1 * size] + src[2 * size]; tmp[1] = src[1 * size] - src[2 * size] - src[3 * size]; tmp[2] = src[4 * size] + src[5 * size] + src[6 * size]; tmp[3] = src[5 * size] - src[6 * size] - src[7 * size]; tmp[4] = src[8 * size] + src[9 * size] + src[10 * size]; tmp[5] = src[9 * size] - src[10 * size] - src[11 * size]; tmp[6] = src[12 * size] + src[13 * size] + src[14 * size]; tmp[7] = src[13 * size] - src[14 * size] - src[15 * size]; dst[col + 0] = tmp[0] + tmp[2] + tmp[4]; dst[col + 1] = tmp[1] + tmp[3] + tmp[5]; dst[dstW + col + 0] = tmp[2] - tmp[4] - tmp[6]; dst[dstW + col + 1] = tmp[3] - tmp[5] - tmp[7]; src++; } dst += 2*dstW; } } }

Количество операций, необходимых для этого преобразования: ~dstH*dstW*dstC*количество .



Особенности практической реализации

В рассмотренном выше примере мы дали описание алгоритма Винограда для блока размером

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

На практике чаще всего используется более продвинутая версия алгоритма:

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

.

В этом случае размер блока будет

Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

, а число преобразованных матриц равно 36. Вычислительный выигрыш согласно формуле (8) достигнет 4 .

Общая схема алгоритма та же, различаются только матрицы алгоритмов преобразования.

Есть небольшой проект на Github , что позволяет вычислять эти матрицы с коэффициентами для произвольного размера ядра свертки и размера блока.

Мы представили версию алгоритма Винограда для формата изображения НЧВ , но алгоритм можно реализовать аналогично для формата НХВК (Подробнее об этих форматах изображений я говорил в предыдущая статья .

Несмотря на наличие дополнительных преобразований, основная вычислительная нагрузка в алгоритме Винограда (если, конечно, правильно его использовать) по-прежнему лежит на умножении матриц.

К счастью, это стандартная операция и эффективно реализована во многих библиотеках.

Функции преобразования, оптимизированные для разных платформ для алгоритма Винограда, можно найти Здесь .



Плюсы и минусы алгоритма Винограда

Сначала, конечно, плюсы:
  • Алгоритм позволяет в несколько раз (обычно в 2-3 раза) ускорить расчет свертки.

    Таким образом, можно вычислить свертку быстрее, чем «теоретический» предел.

  • В своей реализации алгоритм опирается на стандартный алгоритм умножения матриц.

  • Его можно реализовать для разных форматов входных изображений: НЧВ , НХВК .

  • Размер буфера для хранения промежуточных значений меньше, чем требуется для алгоритма, основанного на умножении матриц.

Ну и куда же без минусов:
  • Алгоритм требует отдельной реализации функций преобразования для каждого размера ядра свертки, а также для каждого размера блока.

    Может это одна из основных причин того, что почти везде реализовано только для свертки с ядром

    Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

    .

  • По мере увеличения размера блока сложность реализации функций преобразования быстро возрастает. Поэтому практически не существует реализаций с размером блока больше

    Сверточный слой: быстрая свертка с использованием метода Шмуэля Винограда

    .

  • Алгоритм накладывает строгие ограничения на параметры свертки.

    шагY = шагY = расширениеY = расширениеX = группа = 1 .

    Хотя теоретически реализовать алгоритм при нарушении этих ограничений можно, на практике он малопригоден из-за низкой эффективности.

  • Эффективность алгоритма снижается, если количество входных или выходных каналов в изображении мало (это связано с затратами на преобразование входных и выходных изображений).

  • Эффективность алгоритма снижается при малых размерах входных изображений (полученные матрицы из входного изображения слишком малы по размеру, и стандартный алгоритм умножения матриц становится для них неэффективным).



выводы

Метод расчета свертки, основанный на алгоритме Винограда, позволяет существенно повысить эффективность вычислений по сравнению с методом, основанным на простом умножении матриц.

К сожалению, он не универсален и достаточно сложен в реализации.

Для ряда частных случаев существуют более быстрые подходы, описание которых хотелось бы отложить до следующих статей этой статьи.

ряд .

Жду отзывов и комментариев читателей.

Надеюсь, вам было интересно! P.S. Этот и другие подходы были реализованы мной в Структура свертки в библиотеке Симд .

Эта основа лежит в основе Синет — фреймворк для запуска предварительно обученных нейронных сетей на ЦП.

Теги: #Машинное обучение #Алгоритмы #C++ #Обработка изображений #simd #сверточный слой #Метод Винограда

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.