Введение

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

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

Итак, нейронные сети невероятно хороши в моделировании сложных отношений. Мы собираемся говорить только о части сети с прямым распространением. Теперь блоком ввода нейронной сети может быть что угодно. Это может быть, например, интенсивность оттенков серого (от 0 до 1) изображения размером 20 на 20 пикселей, которое представляет собой набор рукописных цифр. В этом случае у вас будет 400 единиц ввода. Здесь у нас есть 2 входных блока, плюс наш +1 блок смещения (для удивительного объяснения того, зачем вообще нужен блок смещения, перейдите сюда). Прямое распространение, по сути, берет каждый входной сигнал из примера (скажем, одно из этих изображений с рукописной цифрой), затем умножает входные значения на вес каждого соединения между блоками / узлами (см. Рисунок 5), затем складывает все продукты всех подключений к узлу, для которого вы вычисляете активацию, берете эту сумму (z) и пропускаете ее через сигмоидальную функцию (см. ниже).

Это дает вам активацию каждой из единиц скрытого слоя. Затем вы делаете то же самое для вычисления следующего слоя, но на этот раз вы используете активации скрытого слоя в качестве «входных» значений. Вы умножаете все единицы активации a² (т. Е. Скрытого слоя) на второй набор весов Theta2, суммируете каждый продукт, подключенный к одной конечной единице вывода, и пропускаете этот продукт через сигмоидальную функцию, чтобы получить окончательные активации вывода a³. g (z) - сигмоидальная функция, а z - произведение входного сигнала x (или активации в скрытых слоях) и веса тета (представленного единственной стрелкой на диаграмме нормальной нейронной сети на рис. 5).

Когда у вас есть все это, вы хотите рассчитать стоимость сети (рисунок 4). Ваша функция затрат, по сути, вычисляет стоимость / разницу между выходной гипотезой h (x) и фактическими значениями y для приведенных примеров. Итак, в примере, который я продолжаю использовать, y - это значение фактической цифры, представленной входными данными. Если в сети есть изображение канала «4», y - это значение «4». Поскольку существует несколько единиц вывода, функция стоимости сравнивает h (x), вывод, с вектором-столбцом, где 4-я строка равна 1, а все остальные нули. Это означает, что блок вывода, представляющий «4», выводит его как истину, а все остальные - ложь. Для вывода 1, 2 или n см. Ниже.

Две сигмы в приведенной выше функции стоимости J (тета) служат для суммирования затрат для каждого отдельного примера, который вы передаете через сеть (m), и для каждого отдельного выходного класса (K). Теперь вы можете сделать это, выполняя каждое вычисление индивидуально, но, как оказалось, способ, которым люди определили умножение матриц, делает его идеальным для одновременного выполнения всех этих вычислений прямого распространения. оптимизировали функции умножения матриц, чтобы нейронная сеть могла выводить гипотезы чрезвычайно эффективно. Написание нашего кода так, чтобы мы выполняли все вычисления одновременно, а не выполняли все в цикле for по всем примерам ввода, - это процесс, называемый векторизацией нашего кода. Это ОЧЕНЬ важно в нейронных сетях, поскольку они уже достаточно дороги в вычислительном отношении, нам не нужны циклы for, которые еще больше замедляют нас.

Пример нашей сети

В нашей сети у нас будет четыре класса, 1, 2, 3, 4, и мы пройдем через каждый шаг этого вычисления. Предположим, что сеть уже обучена, и наши тета-параметры / веса уже обучены посредством обратного распространения. Это будет трехуровневая сеть (с двумя модулями ввода, 2 модулями скрытого слоя и 4 модулями вывода). Сеть и параметры (также известные как веса) можно представить следующим образом.

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

Начнем со всех наших данных. Наши 3 примера данных и соответствующие выходные значения y. Эти данные ничего не представляют, это просто числа, показывающие, какие расчеты мы будем делать:

Конечно, как я уже упоминал, поскольку существует 4 выходных блока, наши данные должны быть представлены в виде матрицы логических векторов для каждого из трех примеров выходных данных. Я работаю в MATLAB, чтобы превратить наш вектор y в матрицу логических векторов:

yv=[1:4] == y;   %creating logical vectors of y values

Также обратите внимание, что в наших данных X недостаточно функций. В нашей нейронной сети, показанной на Рисунке 5, у нас есть этот блок смещения пунктирной линии x (0), который необходим, когда мы вычисляем произведение весов / параметров и входного значения. Это означает, что нам нужно добавить к данным наши единицы смещения. То есть мы добавляем столбец в начало матрицы:

X = [ones(m,1),X];

Данные X определяются как первые значения активации первого входного уровня a¹, поэтому, если вы когда-либо видите a¹ в коде (строка 3), это просто ссылка на исходные входные данные. Наши веса или параметры каждого из соединений / стрелок в сети следующие:

Ниже приведен полный код, который мы будем использовать для вычисления нашей функции логистической стоимости, мы рассмотрели строки 2 и 9, но мы постепенно разберем умножение матриц и важные манипуляции с матрицами в остальной части этого кода:

1:  m = size(X, 1);
2:  X = [ones(m,1),X]; 
3:  a1 = X;
4:  z2 = Theta1*a1';
5:  a2 = sigmoid(z2);
6:  a2 = [ones(1,m);a2];
7:  z3 = Theta2*a2;
8:  a3 = sigmoid(z3);
9:  yv=[1:4] == y;
10: J = (1/m) * (sum(-yv’ .* log(a3) — ((1 — yv’) .* log(1 — a3))));
11: J = sum(J);

Давайте сделаем первый шаг прямого распространения, строка 4 в приведенном выше коде. Умножение входного значения для каждого примера на их соответствующие веса. Я всегда представляю, как входное значение течет внутрь и вдоль стрелки в нашей сети (рис. 5), ударяется / умножается на вес, а затем ожидает в блоке / узле активации, пока другие стрелки произведут свое умножение. Затем все значение активации для конкретной единицы сначала складывается из суммы каждой из этих стрелок (ввод x вес), затем эта сумма передается через сигмовидную функцию (см. Рисунок 1 выше).

Итак, здесь легко сделать вашу первую ошибку при матричном умножении. Поскольку наши данные с добавленными к X единицами смещения (также называемыми здесь a1) представляют собой матрицу 3x3, а наши Theta1 - это матрица 2x3. Было бы легко просто матрично умножить эти два вместе, поскольку два внутренних измерения Theta: 2x3 и X: 3x3 одинаковы, и поэтому viola, который должен работать правильно и давать нам результирующую матрицу 2x3? Неправильный!

z2 = Theta1 * a1;      %WRONG! THIS DOESN'T GIVE US WHAT WE WANT

Хотя выполнение этого вычисления приведет к выводу матрицы правильного измерения, которое мы ожидаем и которое нам понадобится на следующем шаге, все вычисленные значения будут неправильными, и, таким образом, все вычисления будут неправильными с этого момента. Кроме того, поскольку не было компьютерной ошибки , будет трудно сказать, почему функция стоимости вашей сети вычисляет неправильную стоимость, если вы даже заметите это. Помните, что когда вы выполняете матричное умножение, каждый элемент ab результирующей матрицы представляет собой сумму скалярного произведения строки в первой матрице строка a на столбец второй матрицы столбец b . Если бы мы использовали приведенный выше код для вычисления z² выше, этот первый элемент в результирующей матрице был бы результатом умножения нашей 1-й строки [0,1 0,3. 0,5] со всем столбцом единиц смещения, [1.000; 1.000; 1.000], что нам бесполезно. Это означает, что нам нужно транспонировать нашу матрицу примеров входных данных, чтобы матрица правильно умножала каждую тэту на каждый вход:

z2 = Theta1*a1';

Матричное умножение этого выглядит следующим образом:

Затем мы поэлементно применяем сигмоидальную функцию к каждому из 6 элементов в матрице z² выше:

a2 = sigmoid(z2);

Это просто дает нам матрицу 2x3 значений активации скрытого слоя для обоих скрытых единиц для каждого из трех примеров:

Поскольку это было сделано как умножение матриц, мы смогли вычислить значения активации скрытых слоев сразу, вместо того, чтобы использовать, скажем, цикл for для всех примеров, что становится чрезвычайно затратным с точки зрения вычислений при работе с большими наборами данных. Не говоря уже о необходимости делать обратное распространение.

Теперь, когда у нас есть значения наших единиц активации во втором слое, они действуют как входные данные для следующего и последнего слоя, выходного слоя. Этот слой имеет новый набор весов / параметров, Theta2, для каждой стрелки на рисунке 5 между 2-м и 3-м слоями, и мы будем делать то же самое, что и выше. Умножьте значение активации (вход) на вес, связанный с каждым узлом активации, отдельно просуммируйте продукты, подключенные к каждому узлу активации, затем прогоните сумму каждого узла активации через сигмоидальную функцию, чтобы получить окончательные результаты. Наш a² выше действует как наши входные данные, а наши веса / параметры следующие:

Мы хотим выполнить следующие вычисления:

z3 = Theta2*a2;

Но прежде чем мы это сделаем, мы должны снова добавить наши единицы смещения к нашим данным, в данном случае активации скрытого слоя a². Если вы снова заметите на рисунке 5, пунктирная линия в скрытом слое, a (0), единица смещения, которая добавляется только при выполнении следующего вычисления. Итак, мы добавляем это в матрицу активаций, показанную на Рисунке 11 выше.

Здесь я представил свою ошибку, которая послужила мотивацией для этого сообщения. Для прямого распространения значений активации мы умножим каждый элемент строки в Theta на каждый элемент столбца в a² и сумму из этих продуктов даст нам единственный элемент результирующей матрицы z³. Часто при структурировании данных вы добавляете единицу смещения в виде столбца, но если бы вы это сделали (что я по глупости сделал), это дало бы нам неверный результат. Итак, мы добавляем единицы смещения в виде строки к a².

a2 = [ones(1,m);a2];

Прежде чем мы запустим умножение матриц для вычисления z³, обратите внимание, что там, где раньше в z² вам приходилось транспонировать входные данные a¹, чтобы они «выровнялись» правильно, чтобы умножение матриц привело к нужным вычислениям. Здесь наши матрицы выстроены так, как мы хотим, поэтому нет транспонирования матрицы a². Это еще одна распространенная ошибка, и ее легко сделать, если вы не понимаете вычислений, лежащих в основе всего этого (в прошлом я был очень виноват в этом). Теперь мы можем запустить умножение матрицы на матрицу 4x3 и 3x3, в результате чего получится матрица выходных гипотез 4x3 для каждого из 3 примеров:

z3 = Theta2*a2;

Затем мы поэлементно применяем сигмовидную функцию к каждому из 12 элементов в матрице z²:

a3 = sigmoid(z3);

Это просто дает нам матрицу 4x3 активаций выходного слоя (также известную как гипотеза) для каждого из выходных модулей / классов:

Отсюда вы просто вычисляете функцию стоимости. Единственное, на что следует обратить внимание, это то, что вам нужно транспонировать матрицу векторов y, чтобы убедиться, что поэлементные операции, которые вы выполняете в функции стоимости, правильно совпадают с каждым примером и с единицами вывода.

Затем мы собираем все это вместе, чтобы вычислить функцию стоимости:

J = (1/m) * (sum(-yv’ .* log(a3) — ((1 — yv’) .* log(1 — a3))));
J = sum(J);

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

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