Метод прямого распространения в подробностях

Метод прямого распространения в подробностях, часть 1

Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка данных: глубокое обучение на языке Python, часть 1».

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

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

Итак, перейдём к постановке задачи.

Как вы знаете, наши учебные данные являются комбинацией входных и целевых переменных. Входные переменные мы обозначаем через X, а целевые – через Y. В данном конкретном случае N = 3, D = 2. Это значит, что в нашем эксперименте мы собрали результаты трёх наблюдений (опытов), и в каждом из них есть два входных признака. Вначале мы рассмотрим двоичную классификацию, а затем рассмотрим мультиклассовую с использованием функции софтмакс.

Наши данные состоят в следующем. Входные признаки таковы: первый признак – имеет ли некоторое лицо техническое образование, второй – сколько часов в день оно тратило на изучение глубокого обучения. Двоичная исходящий признак – достигло ли данное лицо успеха в глубоком обучении. Как мы видим, один из входных признаков – имеет ли лицо техническое образование – является двоичным; установим его значение равным единице, если утверждение истинно, и нулю – если ложно. Мы также видим, что второй признак – сколько часов в день тратилось на изучение глубокого обучения – должен быть положительным вещественным числом. К примеру, вы можете тратить 1,5 часа в день на глубокое обучение, но не можете потратить на него -1 час в день.

В нашем случае лицо №1 технического образования не имеет, тратил по 3,5 часа в день на изучение глубокого обучения и достиг в этом успеха. Лицо №2 имеет техническое образование, тратил по 2 часа в день на глубокое обучение и также достиг успеха. Лицо №3 также имеет техническое образование, но тратил на занятия по глубокому обучению всего по 0,5 часа в день и успеха не достиг. Это и есть наш набор данных:

X =\begin{bmatrix}0&3,5\\1&2\\1&0,5\end{bmatrix}_{3x2} , \;Y = \begin{bmatrix}1\\1\\0\end{bmatrix}_{3x1}.

Нужно всегда иметь в виду одну важную вещь. Когда мы делаем прогноз, мы не используем целевые переменные. Целевые переменные используются лишь во время обучения. Когда же делается прогноз, мы берём набор входных переменных и при помощи нейронной сети вычисляем значения исходящих переменных, надеясь при этом, что результаты прогноза будут близки к значению целевых переменных. В следующем разделе мы подробно рассмотрим процесс обучения и его цель – сделать так, чтобы результаты прогнозов были близки к значениям целевых переменных.

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

Теперь рассмотрим весовые коэффициенты нейронной сети. Поскольку каждый входной узел должен быть связан с каждым узлом скрытого слоя, то у нас должно быть дважды по три, то есть шесть, весовых коэффициентов от входного слоя к скрытому. Мы можем выписать их по отдельности или в качестве матрицы размерностью 2×3:

W_{11} = 0.5\\W_{12} = 0.1\\W_{13} = -0.3\\W_{21} = 0.7\\W_{22} = -0.3\\W_{23} = 0.2

или

W =\begin{bmatrix}0.5&0.1&-0.3\\0.7&-0.3&0.2\end{bmatrix}_{2*3}.

Что замечательно в индексах весовых коэффициентов – так это то, что они показывают, какие узлы они связывают. Так, весовой коэффициент Wij показывает, что с входной стороны он идёт от узла i, а с исходящей – к узлу j. Другими словами, коэффициент Wij соединяет узел i предыдущего слоя с узлом j последующего.

У нас также есть свободные члены; они прибавляются к результату после умножения, поэтому у нас есть три свободных члена. Обозначим их через b. Опять же, b могут быть представлены как три отдельные скалярные величины или в виде вектора размерности 3:

b_1 = 0.4\\b_2=0.1\\b_3=0

или

b = \begin{bmatrix}0.4\\0.1\\0\end{bmatrix}_{3*1}.

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

Далее рассмотрим исходящие весовые коэффициенты. Поскольку каждый скрытый узел должен быть связан с каждым исходящим узлом, а у нас три скрытых узла и один исходящий, то получится, что нам нужны лишь три весовых коэффициента, идущих от скрытого слоя к исходящему. Обозначим их через V. Вновь-таки, мы может представить каждый элемент отдельно или в виде вектора:

V_1 = 0.8\\V_2=0.1\\V_3=-0.1

или

V = \begin{bmatrix}0.8\\0.1\\-0.1\end{bmatrix}_{3*1}.

И не забывайте, что мы точно так же определить свободный член. Поскольку свободный член добавляется уже после умножения на V, нам достаточно одного свободного члена, так как у нас всего один исходящий узел. Другими словами, это скалярная величина. Обозначим её через c:

c = 0.2

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

Рассмотрим сначала первое наблюдение:

x = \begin{bmatrix}0\\3.5\end{bmatrix}_{2*1}

и вычислим предсказываемое значение

Прежде всего мы рассчитаем всё с помощью суммы, а в качестве функции активации используем гиперболический тангенс:

z_1 = tanh (\sum_{i=1}^{2} W_{i1} x_i + b_1).

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

Обратите внимание, что каждый член суммы соответствует ребру в нейронной сети так же, как два индекса W соответствуют тому, какой входной и какой исходящий узлы он соединяет. Так, W11 соединяет x1 с z1, W21 – соединяет x2 с z1:

z_1 = tanh (0.5 * 0 + 0.7 * 3.5 + 0.4) = tanh (2.85) = 0.993.

Сделаем теперь то же самое с z2 и z3:

z_2 = tanh (\sum_{i=1}^{2} W_{i2} x_i + b_2),

z_3 = tanh (\sum_{i=1}^{2} W_{i3} x_i + b_3).

Вот правильные ответы:

z_2 = tanh (0.1 * 0 - 0.3 * 3.5 + 0.1) = tanh (-0.95) = -0.740,

z_3 = tanh (-0.3 * 0 + 0.2 * 3.5 + 0) = tanh (0.7) = 0.604.

Итак, теперь у нас есть все значения z скрытого слоя:

z = \begin{bmatrix}0.993\\-0.740\\0.604\end{bmatrix}_{3*1}.

Теперь нам нужно найти p (Y=1|X). Для этого найденные значения необходимо перемножить на V и добавить c, после чего из полученного результата взять сигмоиду:

p (Y=1|X) =\sigma (\sum_{j=1}^{3} V_j \;z_j + c).

Как и в случае с весовыми коэффициентами от входных узлов к скрытым, каждое V так же соединяет один узел скрытого слоя с исходящим узлом, так что V1 соединяет z1 с исходящим узлом, V2 соединяет с исходящим узлом z2, а V3 соединяет исходящий узел с z3:

p (Y=1|X) = \sigma (0.8*0.993 + 0.1 * (-0.74) - 0.1 * 0.604 +0.2);

p (Y=1|X) = \sigma (0.86) = 0.7.

Таким образом, наш прогноз: лицо №1 имеет 70%-й шанс достичь успеха в глубоком обучении.

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

Заметьте, что при использовании записи в векторной и матричной форме мы приходим к виду:

Чтобы удостовериться, что всё это работает, подставим числа в данное выражение и получим ожидаемый ответ:

Разумеется, теперь мы хотим сделать то же самое с переходом от скрытого до исходящего слоя. Так и сделаем:

p (Y=1|X) = \sigma (\sum^{3}_{j=1}V_j z_j + c) = \sigma (V^T z +c).

Подставив числа, вы сможете убедиться, что получится ожидаемый ответ 0,70:

p (Y=1|X) = \sigma ([0.8 \;\;\;\; 0.1 \;\;\; -0.1] \begin{bmatrix}0.993\\-0.740\\0.604\end{bmatrix} + 0.2)

p (Y=1|X) = 0.7.

Теперь, когда мы разобрались, как работать эффективнее путём векторизации каждой операции, возникает следующий очевидный вопрос: можно ли проделать то же самое сразу для нескольких примеров? Вспомните, что мы рассмотрели лишь один пример, тогда как у нас их три, а отдельно вычислять y1, y2 и y3 – это отнюдь не то, чем мы хотели бы заниматься:

X = \begin{bmatrix}0&& 3.5\\1&&2\\1&&0.5\end{bmatrix}_{3*2},

y_1 = \sigma (V^T tanh (W^T x_1 + b) + c)\\y_2 = \sigma (V^T tanh (W^T x_2 + b) + c)\\y_3 = \sigma (V^T tanh (W^T x_3 + b) + c).

Вместо этого мы можем объединить эти операции и одновременно вычислить y1, y2 и y3. Не забывайте, что в сигмоиде и гиперболическом тангенсе вычисления идут поэлементно, поэтому аргумент в этих функциях может быть любого размера:

Y= \begin{bmatrix}y_1\\y_2\\y_3\end{bmatrix} = \sigma (tanh (XW + b) V+c).

Может показаться странным, что в формуле W, X и V поменялись местами. Действительно, почему у нас теперь XW вместо WTX?

Объяснение кроется в том, как именно записывается каждое наблюдение. Когда мы рассматриваем матрицу данных с несколькими наблюдениями и несколькими входными признаками, она записывается в виде матрицы размерности NxD, в которой каждая строка представляет одно наблюдение. Но когда мы рассматриваем это наблюдение как отдельный вектор, оно рассматривается как столбец, поскольку векторы в линейной алгебре рассматриваются именно как векторы-столбцы. Поэтому не забывайте, что в полной матрице данных каждое наблюдение идёт по горизонтали, тогда как если наблюдения рассматриваются по отдельности, они идут вертикально:

X= \begin{bmatrix}0&&3.5\\1&&2\\1&&0.5\end{bmatrix}_{3*2}, \;\;x = \begin{bmatrix}0\\3.5\end{bmatrix}_{2*1}.

Чтобы вы твёрдо удостоверились, что всё именно так, давайте рассмотрим нейронную сеть произвольного размера. Пусть при этом количество входных признаков равно D, а количество скрытых единиц равно M. Поскольку каждый вход должен быть соединён с каждым выходом, то размерность W должна быть равной DxM. Если у нас есть N наблюдений, то размерность X будет равна NxD. Не забывайте «золотое правило» перемножения матриц: внутренние размерности должны совпадать, поэтому у нас есть только один способ их перемножить. Если мы умножаем X на W, их внутренние размерности будут равны D, что допускает умножение. Результат будет размерности NxM. Это и понятно, поскольку в скрытом слое мы должны получить M-размерный вектор для каждого из N наблюдений:

Result_{N*M} = X_{N*D} W_{D*M}.

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

Как я и обещал, теперь мы перейдём от сигмоиды к софтмакс. При переходе от сигмоиды к софтмаксу полезно рассмотреть целевую переменную. Поскольку при использовании софтмакса у нас есть несколько исходящих узлов, то и исходящая переменная становится вектором. В частности, если у нас есть K классов, мы имеем K исходящих узлов и, следовательно, исходящий вектор будет иметь размерность K. Это также значит, что когда мы рассматриваем одновременно несколько входных переменных, представляющих матрицу размерности NxD, то и исходящая переменная будет иметь вид матрицы, но размерности NxK.

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

Ответ в том, что исходящие переменные в нейронной сети при использовании софтмакс удобнее выражать в виде матрицы показателей, что, кстати, эквивалентно прямому кодированию (one-hot encoding), с которым вы познакомились в моём курсе по линейной регрессии.

Прежде чем мы перейдём к прямому кодированию, давайте рассмотрим представление меток. Метки могут иметь большое значение в реальной жизни. К примеру, ваш набор данных может состоять из изображений БМВ и «ягуаров», а задача классификации заключаться в различии между БМВ и «ягуарами» на изображениях. Как вы уже знаете, при двоичной классификации мы можем закодировать их как 0 и 1; при этом совершенно очевидно, что нет никакой разницы, что будет представлено нулём, а что – единицей. БМВ может быть назначено значение 1, а «ягуару» 0, и наоборот. Это совершенно неважно.

Такое представление удобно, поскольку значение сигмоиды всегда находится в диапазоне между 0 и 1. Результат прогноза зависит от того, к какому значению ближе значение исходящей переменной. Например значение 0,75 значит, что мы должны отнести экземпляр к классу 1, а значение 0,49 – что к классу 0. Но что делать, если у нас более двух меток?

Когда у нас более двух меток, то логично просто продолжить счёт от нуля и единицы – так, если у нас три метки, то целевая переменная может принимать значения 0, 1 и 2. Например, если у нас три метки – БМВ, «ягуар» и «фольксваген», то мы можем пометить их как 0, 1 и 2. Если у нас четыре метки, то целевая переменная принимает значения 0, 1, 2 и 3.

Разница в том, что в отличие от ситуации с двоичной классификацией, мы не округляем результат. Так, мы никогда, получив результат 2,7, не округляем его до трёх, потому что софтмакс так не работает. Не забывайте, функция софтмакс отображает вероятность, а потому её значения всегда находятся в диапазоне от 0 до 1.

Всё дело в том, как мы будем использовать эти целевые переменные. Как именно мы этом будем делать, будет объяснено чуть позже. Пока же достаточно помнить о том, если у нас есть массив A длиной K, то первый элемент массива может быть представлен как A[0], второй элемент – как A[1] и так далее до A[K-1].

Вернёмся теперь к методу прямого кодирования. Если вы не помните метод прямого кодирования, то кратко повторим его суть на следующем примере. Предположим, у нас есть список целевых категорий:

\begin{bmatrix}0\\5\\1\\3\\1\\4\\2\\0&\end{bmatrix}.

Ясно, что мы имеем 6 различных категорий, поскольку у нас встречаются цифры от 0 до 5, и 8 наблюдений, поскольку размер массива равен 8. Не забывайте, что наименования категорий от 0 до K-1 совершенно условно. Так, 0 может представлять БМВ, 1 – представлять «ягуар», 2 – представлять «фольксваген» и так далее. Очевидно, что мы не можем просто использовать БМВ, «ягуар» и «фольксваген» для индексирования вектора или матрицы, поскольку они являются строками, и именно поэтому мы кодируем их, используя числа от 0 до K-1. Итак, если у нас есть 6 различных категорий, то мы имеем 6 различных исходящих переменных нейронной сети, представленных, как вы можете видеть, шестью столбцами.

Если вы когда-либо пользовались библиотекой Sci-Kit Learn, то, вероятно, помните, что раньше было необходимо кодировать целевые переменные целыми числами от 0 до K-1. Сейчас эта библиотека поддерживает и строки в качестве целевых переменных, но по внутреннему отображению она по-прежнему преобразовывает строки в целые числа 0 до K-1.

Важно иметь в виду, что хоть мы и кодируем целевые переменные с помощью чисел 0 до K-1, но это вовсе не значит, что эти числа как-либо взаимосвязаны друг с другом. Допустим, 0 означает БМВ, 1 означает «ягуар», а 3 означает «фольксваген». Мы знаем, что единица ближе к нулю, чем тройка, но значит ли это, что «ягуар» более похож на БМВ, чем «фольксваген»? Конечно же, нет, ведь мы могли закодировать марки автомобилей и каким-либо другим способом, например, обозначить «фольксваген» единицей, а «ягуар» тройкой. Это лишь числа, представляющие категории, но сами числа никак не взаимосвязаны между собой – это просто различные символы.

Итак, вернёмся к прямому кодированию. У нас есть 8 примеров и 6 классов. Что это значит? Это значит, что пример №1 помечен как категория 0, пример №2 помечен как категория 5, пример №3 помечен как категория 1 и так далее. Мы хотим применить прямое кодирование. Это значит, что нам необходимо поместить данные в матрицу размером 8×6, причём первоначальные целевые переменные показывают нам, куда подставить единицу в матрице показателей.

Поскольку первая строка представляет категорию 0, то нулевой элемент первой строки матрицы показателей должен быть равен 1. Поскольку вторая строка представляет категорию 5, то пятый элемент второй строки матрицы показателей должен быть равен 1. Не забывайте, что матрица показателей содержит лишь нули и единицы. Другой способ кратко записать это правило:

Показатель(n, k) = 1, если Y(n) = k, иначе 0.

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

В качестве входных данных мы берём одномерный массив Yin, содержащий метки от 0 до K-1. Мы также должны ввести N – количество примеров, являющееся длиной массива Yin, а также K, равного максимальному значению Yin. Далее мы инициируем матрицу нулей размерности NxK, а затем для каждой целевой переменной Yin устанавливаем единицу в соответствующей строке исходящей матрицы Yout.

def convert_numbered_targets_to_indicator_matrix(Yin):

N = length(Yin)

K = max(Yin) + 1

Yout = zeros (N, K)

for n in xrange(N):

Yout[n, Yin[n]] = 1

return Yout

Теперь должно быть понятно, почему значения Yin должны быть от 0 до K-1 – потому что они являются индексами в матрице показателей Yout, вторая размерность которой равна K.

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

Соответственно, в целевой матрице показателей, которая на самом деле является матрицей вероятностей, вероятность того что метка истинна, даёт единицу, а вероятность того, что метка принадлежит другому классу, равна 0. Это потому, что значения целевых переменных уже известны.

Метод прямого распространения в подробностях, часть 2

Исходящие вероятности нейронной сети, конечно же, будут числами между нулём и единицей, поскольку они являются прогнозом, а не определённой величиной. Но цель в том и состоит, чтобы после обучения вероятность истинной целевой переменной была выше, чем вероятность любой другой метки. К примеру, целевой показатель может иметь вид [0, 0, 1, 0, 0], что означает, что целевая переменная помечена категорией 2, и после обучения мы хотим, чтобы исходящий прогноз имел вид вроде [0.1, 0.1, 0.5, 0.2, 0.1], так чтобы максимальная вероятность соответствовала целевой метке. Это и будет целью во время обучения модели.

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

К примеру, выделенная ячейка таблицы показывает вероятность того, что Y1 при заданном X1 принадлежит к категории 0. Следующая ячейка показывает вероятность того, что Y2 при заданном X2 принадлежит к категории 1.

Разберём простой пример, чтобы определить, соответствуют ли прогнозы целевым переменным. Слева у нас матрица вероятностей, представляющая прогнозы. Заметьте, что сумма значений в каждой строке равна единице. Справа у нас целевые переменные. Технически говоря, и здесь сумма значений в каждой строке равна единице, поскольку это также вероятности.

В первой строке 0,7 является максимальным числом и соответствует расположению единицы в таблице целевых переменных. Следовательно, этот прогноз правильный. Во второй строке максимальным значением является 0,4 и находится оно в левом столбце. Но оно соответствует нулю в таблице целевых переменных, а потому этот прогноз неверен. В третьей строке максимальным значением является 0,6. Оно находится в правом столбце и соответствует единице в таблице целевых переменных, поэтому и этот прогноз правилен. В четвёртой строке максимумом является 0,5 и соответствует нулю в таблице целевых переменных – следовательно, прогноз неверен.

В целом у нас два правильных ответа из четырёх. Этот показатель называется коэффициентом классификации или точностью классификации. В данном случае он равен 50%.

Обратите внимание, что если бы мы писали код для этой операции, то на самом деле нам пришлось бы использовать функцию argmax, а не max. Не забывайте, что функция max возвращает наибольшее значение в массиве, тогда как argmax указывает положение наибольшего значения. В Numpy это можно реализовать следующим образом:

prediction_labels = np.argmax(softmax_outputs, axis=1)

target_labels = np.argmax(target_indicator, axis=1)

accuracy = sum(prediction_labels == target_labels)/N

Запись axis=1 означает, что мы берём argmax вдоль столбцов, а не по всей матрице.

Важно отметить, что функция argmax является обратным преобразованием массива целых чисел со значениями от 0 до K-1 в матрицу показателей. Поэтому один из способов убедиться в том, что прямое кодирование правильно – это взять уже закодированные метки и использовать функцию argmax. Результат должен быть равным первоначальным данным:

Y == argmax(convert2indicator(Y), axis=1)

Теперь, когда вы уже знаете, как преобразовать список целевых переменных в матрицу показателей, давайте проделаем то же с нашим первоначальным набором данных. Напомним, что у нас было три примера и два класса, что значит, что наша матрица показателей должна иметь размерность 3×2.

Y= \begin{bmatrix}1\\1\\0\end{bmatrix}_{3*1} \rightarrow Indicator = \begin{bmatrix}0&&1\\0&&1\\1&&0\end{bmatrix}_{3*2}.

Поскольку пример №1 имеет метку 1, то в матрице показателей единица должна быть в элементе с индексом 1. Так как пример №2 также имеет метку 1, то в матрице показателей единица также должна быть в элементе с индексом 1. И поскольку пример №3 имеет метку 0, то в матрице показателей единица должна стоять на месте элемента с индексом 0.

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

Prediction = \begin{bmatrix}0.01&&0.99\\0.02&&0.98\\0.99&&0.01\end{bmatrix}_{3*1} \rightarrow Indicator = \begin{bmatrix}0&&1\\0&&1\\1&&0\end{bmatrix}_{3*2}.

Тут нужно всегда иметь в виду кажущуюся несколько странной нумерацию. Действительно, некоторые вещи мы считаем, начиная с нуля, – например, классы нумеруются 0, 1, 2 и так далее. Другие же вещи мы считаем, начиная с единицы – например, объекты x1 и x2 или z1, z2, z3. Всё это осложняется ещё и тем обстоятельством, что некоторые языки, такие как MATLAB, фактически пользуются нумерацией, начинающейся с единицы, тогда как большинство компьютерных языков используют нумерацию, начинающуюся с нуля.

Итак, почему же мы так поступаем? Это связано с тем, что машинное обучение представляет собой сочетание математики и программирования. Если вы возьмёте какой-либо учебник по математике или статистике, то увидите, что счёт там начинается с единицы. В частности, если идёт суммирование, то вы увидите, что в большинстве случаев я считают от 1 до n, а не от 0 до n – 1. Но если вы возьмёте какой-либо учебник по программированию, вы заметите, что счёт там начинается с нуля. Поскольку машинное обучение является сочетанием и того, и другого, вы должны уметь пользоваться обеими системами отсчёта. Обычно, когда мы рассматриваем уравнения, мы начинаем счёт с единицы, но если мы говорим о деталях программной реализации, мы начинаем отсчёт с нуля. Что касается меток, то они нужны для индексации массивов, и поэтому мы начинаем счёт с нуля.

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

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

Не забывайте, что теперь у нас два исходящих узла, поэтому матрица весовых коэффициентов V должна иметь размерность 3×2, а свободные члены c, которые добавляются к каждому исходящему узлу, теперь представлены вектором размерности 2. Поскольку мы не знаем значений этих весовых коэффициентов, для определённости я установил их следующими:

V = \begin{bmatrix}0&&0.8\\0.4&&0.5\\0.5&&0.4\end{bmatrix}_{3*2}, \;\;\; c = \begin{bmatrix}0.3\\0.5\end{bmatrix}_{2*1}.

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

x= \begin{bmatrix}0\\3.5\end{bmatrix}_{2*1}.

Поскольку значения W и b также остались прежними, то и значение z остаётся неизменным:

Итак, начнём вычисления с этого места. Проводить их будем простым способом от узла к узлу, вместо того чтобы вывести уравнение сразу для всего выхода. Будем проводить их по частям. Выражение для функции активации, связанного с исходящим узлом, который относится к классу 0, равно:

a_1 = \sum_{j=1}^{3} V_{j1} Z_j + c_1.

Выражение для функции активации, связанного с исходящим узлом, который относится к классу 1, равно:

a_2 = \sum_{j=1}^{3} V_{j2} Z_j + c_2.

Не забывайте, что эти a – это то, что мы получаем непосредственно перед подстановкой в функцию софтмакс.

Итак, вычислим наши a1 и a2:

a_1 = 0*0.993 + 0.4* (-0.740) + 0.5 * 0.604 + 0.3 = 0.306.

a_2 = 0.8*0.993 + 0.5* (-0.740) + 0.4 * 0.604 + 0.5 = 1.166.

Теперь, имея значения a1 и a2, мы можем вычислить значения функции софтмакс и, следовательно, исходящие вероятности:

Output \;1= p (y=0|x) = \frac {e^{0.306}} {e^{0.306} + e^{1.166}} = 0.30,

Output \;2 = p (y=1|x) = \frac {e^{1.166}} {e^{0.306} + e^{1.166}} = 0.70.

Неудивительно, что мы получили такие же вероятности, как и прежде, поскольку я специально подобрал соответствующие значения V и c. Как вы помните, в лекции «Сигмоида vs. софтмакс» я показал, как выбрать такие исходящие весовые коэффициенты, чтобы использование функции софтмакс было эквивалентно использованию сигмоиды, поэтому вы, вероятно, захотите просмотреть эту лекцию и убедиться, что мои новые значения V и c следуют указанному правилу.

Как и прежде, мы могли всё это сделать за один раз, используя векторную операцию:

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

И конечно, мы можем пойти ещё дальше и вычислить исходящую величину для всех примеров одновременно. На этом месте выражение начинает походить на выражение с сигмоидой, только вместо сигмоиды стоит софтмакс:

p (y|x) = softmax (tanh (XW + b) V+c).

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

Напомним, что если размерность наших входных признаков равна D, а количество скрытых единиц равно M, то W должно быть матрицей размерности DxM. При этом поскольку свободный член b прибавляется к произведению WX, то он должен быть вектором размерности M. Таким образом, если мы пытаемся вычислить исходящую величину для пакета из N примеров, произведение XW будет иметь размерность NxM.

Итак, что же будет после того, как мы перемножим X и W? Прежде всего напомним, что такое матричное умножение допустимо, так как внутренние размерности, равные D, совпадают с обоих сторон. Результатом этого произведения будет матрица размерностью NxM. Но b при этом является лишь вектором размерности M!

Напомним, что сложение матриц является поэлементным. Это значит, что когда вы складываете две матрицы, они должны иметь одинаковую размерность. Поэтому операция XW + b, о которой мы говорили ранее, математически некорректна. Чтобы всё получилось и свободный член b можно было добавить ко всем N входным примерам, мы должны «перевернуть» b, чтобы он стал горизонтальным, и повторить всё это N раз. Это даст нам матрицу размерности NxM, с которой можно проводить операцию сложения:

XW_{N*M} + \begin{bmatrix}-b^T-\\-b^T-\\...\\-b^T-\end{bmatrix}_{N*M}.

К счастью, библиотека Numpy не требует для этого явного указания и сразу понимает, что должна добавить одно и то же значение b к каждой строке результата, так что в конечном счёте никакой дополнительной работы не требуется, достаточно написать

X.dot(W) + b.

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту
Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Share via
Copy link