Создание нейронной сети с нуля в Python: Многоклассовая классификация
Это третья статья в серии статей на тему “Создание нейронной сети с нуля в Python”.
- Создание нейронной сети с нуля в Python
- Создание нейронной сети с нуля в Python: Добавление скрытых слоев
- Создание нейронной сети с нуля в Python: Многоклассовая классификация
Если у вас нет предыдущего опыта работы с нейронными сетями, я бы посоветовал вам сначала прочитать Часть 1 и часть 2 серии (ссылка выше). Как только вы почувствуете себя комфортно с понятиями, описанными в этих статьях, вы можете вернуться и продолжить эту статью.
Вступление
В предыдущей статье мы видели , как мы можем создать нейронную сеть с нуля, которая способна решать задачи двоичной классификации, в Python. Задача двоичной классификации имеет только два выхода. Однако реальные проблемы гораздо сложнее.
Рассмотрим пример задачи распознавания цифр, где мы используем изображение цифры в качестве входных данных, а классификатор предсказывает соответствующее число цифр. Цифра может быть любым числом от 0 до 9. Это классический пример многоклассовой задачи классификации, где вход может принадлежать любому из 10 возможных выходов.
В этой статье мы увидим, как можно создать простую нейронную сеть с нуля на Python, которая способна решать задачи многоклассовой классификации.
Набор данных
Давайте сначала кратко взглянем на наш набор данных. Наш набор данных будет иметь два входных объекта и один из трех возможных выходных. Мы вручную создадим набор данных для этой статьи.
Для этого выполните следующий сценарий:
import numpy as np import matplotlib.pyplot as plt np.random.seed(42) cat_images = np.random.randn(700, 2) + np.array([0, -3]) mouse_images = np.random.randn(700, 2) + np.array([3, 3]) dog_images = np.random.randn(700, 2) + np.array([-3, 3])
В приведенном выше сценарии мы начинаем с импорта наших библиотек, а затем создаем три двумерных массива размером 700 х 2. Вы можете думать о каждом элементе в одном наборе массива как об изображении конкретного животного. Каждый элемент массива соответствует одному из трех выходных классов.
Здесь важно отметить, что если мы построим элементы массива cat_images
на двумерной плоскости, то они будут центрированы вокруг и y=-3. Аналогично, элементы массива mouse_images
будут центрированы вокруг и, наконец, элементы массива dog_images
будут центрированы вокруг x=-3 и. Вы увидите это, как только мы построим наш набор данных.
Затем нам нужно вертикально соединить эти массивы, чтобы создать наш окончательный набор данных. Для этого выполните следующий сценарий:
feature_set = np.vstack([cat_images, mouse_images, dog_images])
Мы создали наш набор функций, и теперь нам нужно определить соответствующие метки для каждой записи в нашем наборе функций. Это делает следующий сценарий:
labels = np.array([0]*700 + [1]*700 + [2]*700)
Приведенный выше скрипт создает одномерный массив из 2100 элементов. Первые 700 элементов были помечены как 0, следующие 700 элементов были помечены как 1, а последние 700 элементов были помечены как 2. Это просто наш короткий способ быстрого создания меток для наших соответствующих данных.
Для задач многоклассовой классификации нам нужно определить выходную метку как вектор с одним горячим кодированием, так как наш выходной слой будет иметь три узла, и каждый узел будет соответствовать одному выходному классу. Мы хотим, чтобы при прогнозировании выходного сигнала значение соответствующего узла было равно 1, а остальные узлы-0. Для этого нам нужно три значения выходной метки для каждой записи. Вот почему мы преобразуем наш выходной вектор в один горячий кодированный вектор.
Выполните следующий скрипт для создания векторного массива с одним горячим кодированием для нашего набора данных:
one_hot_labels = np.zeros((2100, 3)) for i in range(2100): one_hot_labels[i, labels[i]] = 1
В приведенном выше скрипте мы создаем массив one_hot_labels
размером 2100 x 3, где каждая строка содержит один горячий кодированный вектор для соответствующей записи в наборе объектов. Затем мы вставляем 1 в соответствующую колонку.
Если вы выполните описанный выше скрипт, то увидите, что массив one_hot_labels
будет иметь 1 в индексе 0 для первых 700 записей, 1 в индексе 1 для следующих 700 записей и 1 в индексе 2 для последних 700 записей.
Теперь давайте построим график набора данных, который мы только что создали. Выполните следующий сценарий:
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5) plt.show()
После выполнения приведенного выше сценария вы должны увидеть следующий рисунок:
Вы можете ясно видеть, что у нас есть элементы, принадлежащие к трем различным классам. Наша задача будет заключаться в разработке нейронной сети, способной классифицировать данные по вышеупомянутым классам.
Нейронная сеть с несколькими выходными классами
Нейронная сеть, которую мы собираемся спроектировать, имеет следующую архитектуру:
Вы можете видеть, что наша нейронная сеть очень похожа на ту, которую мы разработали во второй части серии. Он имеет входной слой с 2 входными объектами и скрытый слой с 4 узлами. Однако в выходном слое мы видим, что у нас есть три узла. Это означает, что наша нейронная сеть способна решать многоклассовую классификационную задачу, где число возможных выходов равно 3.
Softmax и Кросс-энтропийные функции
Прежде чем перейти к разделу кода, давайте кратко рассмотрим функции softmax и cross entropy , которые соответственно являются наиболее часто используемыми функциями активации и потерь для создания нейронной сети для многоклассовой классификации.
Функция Softmax
Из архитектуры нашей нейронной сети мы можем видеть, что у нас есть три узла в выходном слое. У нас есть несколько вариантов функции активации на выходном уровне. Один из вариантов-использовать сигмоидную функцию, как мы это делали в предыдущих статьях.
Однако существует более удобная функция активации в виде softmax, которая принимает вектор в качестве входного и производит другой вектор той же длины, что и выходной. Поскольку наш выход содержит три узла, мы можем рассматривать выход из каждого узла как один элемент входного вектора. Выход будет представлять собой длину одного и того же вектора, где значения всех элементов суммируются до 1. Математически функцию softmax можно представить в виде:
Функция softmax просто делит показатель каждого входного элемента на сумму показателей всех входных элементов. Давайте рассмотрим простой пример этого:
def softmax(A): expA = np.exp(A) return expA / expA.sum() nums = np.array([4, 5, 6]) print(softmax(nums))
В приведенном выше скрипте мы создаем функцию softmax, которая принимает в качестве входных данных один вектор, принимает показатели всех элементов вектора, а затем делит полученные числа по отдельности на сумму показателей всех чисел во входном векторе.
Вы можете видеть, что входной вектор содержит элементы 4, 5 и 6. На выходе вы увидите три числа, сжатые между 0 и 1, где сумма чисел будет равна 1. Выход выглядит примерно так:
[0.09003057 0.24472847 0.66524096]
Функция активации Softmax имеет два основных преимущества по сравнению с другими функциями активации, особенно для задач многоклассовой классификации: первое преимущество заключается в том, что функция softmax принимает вектор в качестве входных данных, а второе преимущество заключается в том, что она выдает выходные данные между 0 и 1. Помните, что в нашем наборе данных у нас есть однократно закодированные выходные метки, которые означают, что наши выходные данные будут иметь значения между 0 и 1. Однако выход процесса прямой передачи может быть больше 1, поэтому функция softmax является идеальным выбором на выходном слое, поскольку она сжимает выход между 0 и 1.
Кросс-энтропийная функция
С помощью функции активации softmax на выходном уровне функция mean squared error cost может быть использована для оптимизации затрат, как это было сделано в предыдущих статьях. Однако для функции softmax существует более удобная функция затрат, которая называется кросс-энтропией.
Математически функция кросс-энтропии выглядит следующим образом:
Кросс-энтропия-это просто сумма произведений всех действительных вероятностей на отрицательный логарифм предсказанных вероятностей. Для задач многоклассовой классификации функция кросс-энтропии, как известно, превосходит градиентную приличную функцию.
Теперь у нас есть достаточно знаний, чтобы создать нейронную сеть, которая решает многоклассовые задачи классификации. Давайте посмотрим, как будет работать наша нейронная сеть.
Как всегда, нейронная сеть работает в два этапа: прямая и обратная передача.
Подача Вперед
Прямая фаза останется более или менее похожей на то, что мы видели в предыдущей статье. Единственное отличие состоит в том, что теперь мы будем использовать функцию активации softmax на выходном слое, а не сигмоидную функцию.
Помните, что для вывода скрытого слоя мы по-прежнему будем использовать сигмоидную функцию, как и раньше. Функция softmax будет использоваться только для активации выходного слоя.
Фаза 1
Поскольку мы используем две различные функции активации для скрытого слоя и выходного слоя, я разделил фазу подачи вперед на две подфазы.
На первом этапе мы увидим, как вычислить выход из скрытого слоя. Для каждой входной записи у нас есть две функции “x1” и “x2”. Чтобы вычислить выходные значения для каждого узла скрытого слоя, мы должны умножить входные значения на соответствующие веса узла скрытого слоя, для которого мы вычисляем значение. Обратите внимание, что мы также добавляем сюда термин предвзятости. Затем мы пропускаем точечный продукт через сигмоидную функцию активации, чтобы получить конечное значение.
Например, чтобы вычислить конечное значение для первого узла в скрытом слое, который обозначается “h1”, необходимо выполнить следующее вычисление:
$$ + $$ + x2 w2 + b $$
$$ $$ ah1 = \frac{\mathrm{1} }{\mathrm{1} + e^{-zh 1} } $$
Это результирующее значение для самого верхнего узла скрытого слоя. Таким же образом можно вычислить значения для 2-го, 3-го и 4-го узлов скрытого слоя.
Фаза 2
Чтобы вычислить значения для выходного слоя, значения в узлах скрытого слоя обрабатываются как входные. Поэтому, чтобы вычислить выход, умножьте значения узлов скрытого слоя на их соответствующие веса и передайте результат через функцию активации, которая в этом случае будет softmax.
Эта операция может быть математически выражена следующим уравнением:
$$ + ah2w10 + ah3w11 + ah4w12 $$
$$ + ah2w14 + ah3w15 + ah4w16 $$
$$ + ah2w18 + ah3w19 + ah4w20 $$
Здесь zo1, zo2 и zo3 образуют вектор, который мы будем использовать в качестве входных данных для сигмоидной функции. Давайте назовем этот вектор “zo”.
zo = [zo1, zo2, zo3]
Теперь, чтобы найти выходное значение a01, мы можем использовать функцию softmax следующим образом:
$$ $$ ao1(zo) = \frac{e^{zo 1}}{}^{k}{e^{zouk}} } $$
Здесь “a01”-это выход для самого верхнего узла в выходном слое. Точно так же вы можете использовать функцию softmax для вычисления значений ao2 и ao3.
Вы можете видеть, что прямой шаг для нейронной сети с многоклассовым выходом очень похож на прямой шаг нейронной сети для задач бинарной классификации. Единственное отличие состоит в том, что здесь мы используем функцию softmax на выходном слое, а не сигмоидную функцию.
Обратное распространение
Основная идея, лежащая в основе обратного распространения, остается прежней. Мы должны определить функцию затрат, а затем оптимизировать эту функцию затрат путем обновления весов таким образом, чтобы затраты были минимизированы. Однако, в отличие от предыдущих статей, где мы использовали среднеквадратичную ошибку в качестве функции затрат, в этой статье мы вместо этого будем использовать функцию кросс-энтропии.
Обратное распространение-это задача оптимизации, в которой мы должны найти минимумы функций для нашей функции затрат.
Чтобы найти минимумы функции, мы можем использовать алгоритм градиентного спуска . Алгоритм градиентного спуска может быть математически представлен следующим образом:
Подробности о том, как градиентная функция минимизирует стоимость, уже обсуждались в предыдущей статье. Здесь мы просто увидим математические операции, которые нам нужно выполнить.
Наша функция затрат такова:
В нашей нейронной сети мы имеем выходной вектор, где каждый элемент вектора соответствует выходу из одного узла в выходном слое. Выходной вектор вычисляется с помощью функции softmax. Если “ao” – вектор прогнозируемых выходов из всех выходных узлов, а “y” – вектор фактических выходов соответствующих узлов в выходном векторе, то мы должны в основном минимизировать эту функцию:
Фаза 1
На первом этапе нам нужно обновить весы w9 до w20. Это веса узлов выходного слоя.
Из предыдущей статьи мы знаем, что для минимизации функции затрат мы должны обновить значения веса таким образом, чтобы стоимость уменьшилась. Для этого нам нужно взять производную функции затрат по отношению к каждому весу. Математически мы можем представить его как:
$$ \$$ \frac {d cost}{down} = \frac {d cost}{dao} *, \frac {dao}{dz} * \frac {dz}{down} ….. (1) $$
Здесь “wo” относится к весам в выходном слое.
Первую часть уравнения можно представить в виде:
$$ \$$ \frac {d cost}{dao} *\ \frac {dao}{dzo} ……. (2) $$
Подробный вывод функции кросс-энтропийных потерь с функцией активации softmax можно найти по ссылке this link .
Производная уравнения (2) имеет вид:
$$ \$$ \frac {d cost}{dao} *\ \frac – y ……. (3) $$
Где “ao” – прогнозируемый выход, а “y” – фактический выход.
Наконец, нам нужно найти “дзо” относительно “два” из Уравнение 1 . Производная – это просто выходные данные, поступающие из скрытого слоя, как показано ниже:
$$ \frac $$
Чтобы найти новые значения веса, значения, возвращаемые уравнением 1 , можно просто умножить на скорость обучения и вычесть из текущих значений веса.
Нам также нужно обновить смещение “bo” для выходного слоя. Нам нужно дифференцировать нашу функцию затрат по отношению к смещению, чтобы получить новое значение смещения, как показано ниже:
$$ \$$ \frac {d cost}{dbo} = \frac {d core}{dao} *\ \frac {dao}{dz} * \frac {dzo}{dbo} ….. (4) $$
Первая часть Уравнения 4 уже была вычислена в Уравнение 3 . Здесь нам нужно только обновить “дзо” по отношению к “бо”, который просто равен 1. Так что:
$$ \frac – y ……….. (5) $$
Чтобы найти новые значения смещения для выходного слоя, значения, возвращаемые уравнением 5 , можно просто умножить на скорость обучения и вычесть из текущего значения смещения.
Фаза 2
В этом разделе мы снова распространим нашу ошибку на предыдущий слой и найдем новые значения веса для весов скрытого слоя, то есть веса от w1 до w8.
Давайте вместе обозначим скрытые веса слоев как “wh”. Мы в основном должны дифференцировать функцию затрат по отношению к “wh”.
Математически мы можем использовать цепное правило дифференцирования, чтобы представить его как:
$$ \$$ \frac {d cost}{dw} = \frac {d cost}{dah} *, \frac {dh}{dz} * \frac {dz}{dw} …… (6) $$
Вот опять мы сломаемся Уравнение 6 в отдельные члены.
Первый термин “стоимость” может быть дифференцирован по отношению к “dah”, используя цепное правило дифференциации следующим образом:
$$ \$$ \frac {d cost}{dah} = \frac {d cost}{dzo} *\ \frac {dot}{dah} …… (7) $$
Давайте снова разберем Уравнение 7 на отдельные члены. Из уравнения |/3 мы знаем , что:
$$ \$$ \frac {d cost}{dao} *\ \frac {dao}{dz} =\frac {d cost}{dzo} – y …….. (8) $$
Теперь нам нужно найти ду/дах из Уравнение 7 , которое равно весам выходного слоя, как показано ниже:
$$ \frac …… (9) $$
Теперь мы можем найти значение cost/dah, заменив значения из Уравнения 8 и 9 в Уравнение 7 .
Возвращаясь к Уравнение 6 , нам еще предстоит найти dh/dz и dz/dw.
Первый член dah/dzh может быть вычислен как:
$$ \frac(zh) * (1-сигмовидная(zh)) …….. (10) $$
И, наконец, dzh/dwh-это просто входные значения:
$$ \особенности frac …….. (11) $$
Если мы заменим значения из Уравнения 7 , 10 и 11 в Уравнение 6 , мы можем получить обновленную матрицу для весов скрытого слоя. Чтобы найти новые значения веса для весов скрытого слоя “wh”, значения, возвращаемые уравнением 6 , можно просто умножить на скорость обучения и вычесть из текущих значений веса скрытого слоя.
Аналогично, производная функции затрат по отношению к смещению скрытого слоя “bh” может быть просто вычислена как:
$$ \$$ \frac {d cost}{dbh} = \frac {d cost}{dah} *, \frac {dh}{dz} * \frac {dz}{db} …… (12) $$
Что просто равно:
$$ \$$ \frac {d cost}{dbh} = \frac {d cost}{dah} *, \frac {dah}{dj} …… (13) $$
потому что,
$$ \frac $$
Чтобы найти новые значения смещения для скрытого слоя, значения, возвращаемые уравнением 13 , можно просто умножить на скорость обучения и вычесть из текущих значений смещения скрытого слоя, и это все для обратного распространения.
Вы можете видеть, что процесс прямого и обратного распространения очень похож на тот, который мы видели в наших последних статьях. Единственное, что мы изменили, – это функция активации и функция стоимости.
Код для нейронных сетей для многоклассовой классификации
Мы рассмотрели теорию, лежащую в основе нейронной сети для многоклассовой классификации, и теперь настало время применить эту теорию на практике.
Взгляните на следующий сценарий:
import numpy as np import matplotlib.pyplot as plt np.random.seed(42) cat_images = np.random.randn(700, 2) + np.array([0, -3]) mouse_images = np.random.randn(700, 2) + np.array([3, 3]) dog_images = np.random.randn(700, 2) + np.array([-3, 3]) feature_set = np.vstack([cat_images, mouse_images, dog_images]) labels = np.array([0]*700 + [1]*700 + [2]*700) one_hot_labels = np.zeros((2100, 3)) for i in range(2100): one_hot_labels[i, labels[i]] = 1 plt.figure(figsize=(10,7)) plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5) plt.show() def sigmoid(x): return 1/(1+np.exp(-x)) def sigmoid_der(x): return sigmoid(x) *(1-sigmoid (x)) def softmax(A): expA = np.exp(A) return expA / expA.sum(axis=1, keepdims=True) instances = feature_set.shape[0] attributes = feature_set.shape[1] hidden_nodes = 4 output_labels = 3 wh = np.random.rand(attributes,hidden_nodes) bh = np.random.randn(hidden_nodes) wo = np.random.rand(hidden_nodes,output_labels) bo = np.random.randn(output_labels) lr = 10e-4 error_cost = [] for epoch in range(50000): ############# feedforward # Phase 1 zh = np.dot(feature_set, wh) + bh ah = sigmoid(zh) # Phase 2 zo = np.dot(ah, wo) + bo ao = softmax(zo) ########## Back Propagation ########## Phase 1 dcost_dzo = ao - one_hot_labels dzo_dwo = ah dcost_wo = np.dot(dzo_dwo.T, dcost_dzo) dcost_bo = dcost_dzo ########## Phases 2 dzo_dah = wo dcost_dah = np.dot(dcost_dzo , dzo_dah.T) dah_dzh = sigmoid_der(zh) dzh_dwh = feature_set dcost_wh = np.dot(dzh_dwh.T, dah_dzh * dcost_dah) dcost_bh = dcost_dah * dah_dzh # Update Weights ================ wh -= lr * dcost_wh bh -= lr * dcost_bh.sum(axis=0) wo -= lr * dcost_wo bo -= lr * dcost_bo.sum(axis=0) if epoch % 200 == 0: loss = np.sum(-one_hot_labels * np.log(ao)) print('Loss function value: ', loss) error_cost.append(loss)
Код очень похож на тот, который мы создали в предыдущей статье. В разделе прямой передачи единственная разница заключается в том, что “ao”, который является конечным выходом, вычисляется с помощью функции softmax
.
Аналогично, в разделе обратного распространения, чтобы найти новые веса для выходного слоя, функция затрат выводится относительно функции softmax
, а не функции sigmoid
.
Если вы запустите приведенный выше скрипт, то увидите, что окончательная стоимость ошибки составит 0,5. На следующем рисунке показано, как стоимость уменьшается с числом эпох.
Как вы можете видеть, не так много эпох требуется, чтобы достичь нашей конечной стоимости ошибки.
Аналогично, если вы запустите тот же сценарий с сигмоидной функцией на выходном слое, минимальная стоимость ошибки, которую вы достигнете после 50000 эпох, будет около 1,5, что больше 0,5, достигнутого с помощью softmax.
Вывод
Реальные нейронные сети способны решать многоклассовые задачи классификации. В этой статье мы увидели, как можно создать очень простую нейронную сеть для многоклассовой классификации с нуля в Python. Это заключительная статья серии: “Нейронная сеть с нуля в Python”. В следующих статьях я объясню, как мы можем создавать более специализированные нейронные сети, такие как рекуррентные нейронные сети и сверточные нейронные сети с нуля в Python.