Основы глубокого обучения. Часть 1. ai.. ai. numpy.. ai. numpy. python.. ai. numpy. python. PyTorch.. ai. numpy. python. PyTorch. глубокое обучение.. ai. numpy. python. PyTorch. глубокое обучение. ИИ.. ai. numpy. python. PyTorch. глубокое обучение. ИИ. искусственный интеллект.. ai. numpy. python. PyTorch. глубокое обучение. ИИ. искусственный интеллект. Машинное обучение.. ai. numpy. python. PyTorch. глубокое обучение. ИИ. искусственный интеллект. Машинное обучение. научно-популярное.

🙂 Приветствую всех! Эта статья будет первой в серии статей про основы глубокого обучения. В этой части я расскажу про то, что такое модели, искусственный интеллект (ИИ), машинное (МО) и глубокое обучение (ГО), про виды этапа обучения моделей, что такое нейронные сети, градиентный спуск и обратное распространение. В конце затронем теорию свёрточных нейронных сетей.

Модель — это функция, закодированная в виде программы (по сути слова синонимы).

Искусственный интеллект — это раздел компьютерных наук, связанный с исследованиями в разных областях, включая доказательства математических теорем, анализ естественного языка, игры, компьютерные программы, способные обучаться на примерах, и нейронные сети. Он включает в себя все направления связанные и с МО, и с ГО, и с другими областями.

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

Алгоритм МО — это поисковый процесс, предназначенный для выбора функции из ряда потенциальных вариантов функций, которая лучше всего объясняет отношения между признаками набора данных.

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

После определения понятий перейдем к процессу машинного обучения.

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

Сейчас я сосредоточусь на этапе обучения модели.

Здесь есть две проблемы: переобучение и недообучение.

  • Переобучение встречается, когда используется слишком малый индуктивный сдвиг (предложение о функции, то есть даём большую свободу в выборе функции нашему алгоритму МО или нейронной сети , чтобы максимально соответствовать набору данных и больше акцентирует внимание на наборе данных). Из-за переобучения модель слишком сильно акцентирует на данный набор данных, может захватывать шумы данных, ошибки в данных, а также получиться слишком сложной.

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

МО имеет три категории в зависимости от набора данных, функций-кандидатов и функции приспособленности или пригодности модели: обучение с учителем, обучение без учителя и обучение с подкреплением.

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

  • МО без учителя подходит для кластеризации данных (объединение образцов из набора данных в группы по схожим признакам). Здесь набор данных не имеет целевого выходного признака. Модель сама учиться составлять образцы в правильные группы по схожим признакам. А в качестве функции приспособленности может выступать функция, которая отдает предпочтение кандидатам, которые достигает высокого сходства внутри кластера.

  • МО с подкреплением используется там, где невозможно или очень затратно составить набор данных (например, научить модель играть в змейку). Тогда модель учится непосредственно в среде. Для обучения и оценки приспособленности могут выступать очки(подкрепление).

Что такое нейронные сети?

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

Визуальное представление структуры нейронной сети

Визуальное представление структуры нейронной сети

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

Взвешенная сумма весов и входных переменных — это умножение входных значений на их веса с последующим сложением (то есть умножение вектора-строки выходов одного слоя на матрицу весов следующего слоя).

Что из себя представляет семейство моделей нейронных сетей (или просто нейронные сети)?

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

Принято считать, что нейроны организованы в виде слоёв, на рисунке выше их четыре: один входной, два скрытых и один выходной.

Сети глубокого обучения — это нейронные сети со множеством скрытых слоёв. Чтобы сеть считалась глубокой скрытых слоёв должно быть не меньше двух.

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

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

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

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

Функция активации нужна для формирования не только линейных, но и нелинейных моделей. Обычно в одном слое все нейроны имеют одинаковую функцию активации.

Также нейрон с определенной функцией активацией называется модулем, например, нейрон с выпрямляющей функцией активации называют выпрямляющим модулем (ReLU).

Что такое градиентный спуск?

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

Отклонение (погрешность) модели можно считать по-разному. Я буду использовать формулу среднеквадратической ошибки (SSE):

SSE=0.5 cdot sum_{i=1}^{n} (y_i - hat{y}_i)^2

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

Куда спускается градиентный спуск?

Чтобы лучше понять эту идею я начну издалека. Пусть у нас есть один нейрон и два входных параметра (признака), обозначим веса для этих параметров w1, w2. Тогда мы можем нарисовать график зависимости SSE от w1 и w2. У этого графика будет точка минимума, указывающая на нужный нам вес модели. В простейшем случае мы инициализируем модель любым весом и считаем градиент функции SSE в этой точке. Напомню, градиент — это скорость изменения функции для заданного ввода, то есть вектор из частных производных по всем параметрам в определенной точке функции. Если градиент большой, то надо двигаться в противоположную от него сторону и с большим шагом, иначе с меньшим шагом.

Теперь давайте в нашей формуле для SSE заменим обозначение предсказанного целевого признака (ŷ) на реальную функцию, которую у нас реализует нейрон. Тогда:

SSE=frac{1}{2} sum_{i=1}^{n} bigl( y_i - (w_1 x_{1i} + w_2 x_{2i}) bigr)^2

Где сумма по всем образцам.

Или можем обобщить её для m входных параметров:

SSE=frac{1}{2} sum_{i=1}^{n} left( y_i - sum_{j=1}^{m} x_{ij} w_j right)^2

Где первая сумма (которая в скобках) по всем признакам в образце, а вторая по всем образцам

Теперь для градиента нам нужны частные производные:

frac{partial SSE}{partial w_i}=-sum_{j=1}^{n} (y_j - hat{y}_j) , x_{ji}

Где сумма по всем образцам и только по признаку i-тому, так как смотрим отклонение модели относительно веса для i-того параметра

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

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

Поэтому в правиле обновления весов методом градиентного спуска знак «-» перед вводом хi опускается. Это правило можно определить так:

w_i=w_i + h sum_{j=1}^{n} (y_j - hat{y}_j) , x_{ji}

Где сумма по всем образцам и значение х именно i-того признака, h – гиперпараметр (то есть выбирается вручную).

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

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

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

Пусть w0 — это сдвиг нейрона, а остальные веса относятся к другим его входным значениям. Вычисление частной производной SSE зависит от структуры функции, которая генерирует ŷ. Чем сложнее эта функция, тем сложнее становится частная производная.

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

Логистическая функция активации имеет очень простую производную по своим входным значениям.

Правило для логистической функции:

w_i=w_i + h cdot sum(y - hat{y}) cdot hat{y}(1 - hat{y}) cdot x_i

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

Начну с пояснения задачи

Представьте, что мы в другой стране, где светофор имеет 3 индикатора. Понаблюдав за ними мы вычленили 6 образцов и составили размеченный обучающий набор данных для нашей модели из 5, а 6 оставили на тестирование модели.

import numpy as np

input_data = np.array([
    [1,0,1],
    [0,1,1],
    [0,0,1],
    [1,1,1],
    [0,1,1],
])

goal_pred = np.array([0,1,0,1,1])
weight = np.array([0.1, 0.1, 0.1]) # лучше начинать не с полных нулей
alpha = 0.1

Здесь матрица input_data содержит индикаторы светофора (в одной строке один образец), а вектор goal_pred содержит соответствующие этим столбцам правильные решения. Альфа-коэффициент выбираете на свой вкус (это тот самый h из формул выше, просто он имеет разные названия).

Создадим наш нейрон

def neiron(input_row, weight):
    return input_row.dot(weight)

Да, всё настолько просто! Если вы помните из прошлых постов, то каждый нейрон считает взвешенную сумму и применяет функцию активации, у нас пример простой и функцию активации можно убрать. Функция dot считает скалярное произведение векторов.

Реализуем градиентный спуск

for i in range(40):
    total_error = 0
    for z in range(len(input_data)):
        pred = neiron(input_data[z], weight)
        error = pred - goal_pred[z]
        sse = error**2
        total_error += sse
        
        weight = weight - (alpha * (input_data[z] * error))
        
    if i % 10 == 0:
        print(f"Итерация номер {i}, Суммарная ошибка: {total_error}")

print("nИтоговые веса:", weight)

Здесь мы используем стохастический градиентный спуск, а не полный как я давал в посте раньше. Разница лишь в том, что тут мы ошибку считаем и обновляем веса после каждого образца, а в полном после всех образцов из набора данных. Обратите внимание на знак (к весу прибавляем или от него вычитаем), он зависит от того, как считалась чистая ошибка error. Так же обычно в реализациях деление на два при подсчёте среднеквадратичной ошибки опускают, так при вычислении производной всё равно сокращается (0.5 * 2).

Как поведёт себя наша модель из одного нейрона на новом образце?

#У меня остался ещё один набор на котором модель не обучалась на [1, 0, 0] должна выдать 0
print("Проверка модели на наборе 1,0,0 где ответ верный 0")
pred = neiron(np.array([1,0,0]),weight)
a = 0 if pred < 0.01 else 1 # по сути тут мы применили пороговую функцию активации
print("Модель дала ответ: " + str(a) )
Результат нашей модели на тестовом образце

Результат нашей модели на тестовом образце

Как вы можете видеть на скриншоте выше, наша модель справилась на отлично!

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

В чем суть обратного распространения?

При прямом проходе (от входа до получения результата) мы записываем взвешенную сумму вводов и выход каждого нейрона.

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

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

Передача дельт в обратном направлении

Напомню, что дельта у нас — это скорость изменения отклонения сети относительно взвешенной суммы какого-то (пусть далее будет k-того) нейрона:

δ=dE / dz=dE / da ⋅ da / dz

Где здесь и далее E — отклонение сети, а — вывод нейрона, z — взвешенная сумма вводов нейрона

Для выходного слоя справедливо: dE/da = t – a, где t — ожидаемый вывод

Для скрытого слоя нельзя привязать вывод к отклонению непосредственно, эту связь можно вычислить как взвешенную сумму значений дельта тех нейронов, которые сразу за ним:

dE / da=∑ w ⋅ δ

Где сумма по всем нейронам следующего слоя.

da/dz вычисляется проще, но она включает в себя производную функции активации, поэтому и используют функции различные, одни проще дифференцируются, а другие не так сильно уменьшают нашу дельту.

Обновление весов

Основной принцип состоит в том, что вес обновляется прямо пропорционально чувствительности отклонения сети относительно этого веса. Возьмём вес между нейроном j и k, частная производная отклонения сети будет: dE/dw = dz/dw × dE/dz

Основы глубокого обучения. Часть 1 - 11

Понятнее будет объяснять на рисунке выше. Начинаем с вывода всей сети, вычисляем дельту для нейрона l по формуле выше, далее умножаем на производную dz/dw (она всегда константа, так как входит только в одну сумму вес) и получаем градиент для этого веса. Далее считаем дельту уже для нейрона k (по формуле выше):

δₖ=daₖ / dzₖ ⋅ (w(k,l) ⋅ δₗ)

Умножая на dzk/dwj,k (то есть на aj), получаем градиент для нашего веса wj,k.

Для градиентного спуска правило обновления будет таким:

w(j,k)=w(j,k) + h ⋅ δₖ ⋅ aⱼ

Теперь мы с вами создадим простую двухслойную нейронную сеть (3 нейрона во входном, 3 в скрытом и 1 в выходном слое), обновлять веса будем с помощью стохастического градиентного спуска.

Начнём с задания весов и входных данных

Здесь мы продолжаем задачу со светофором. Нужно определить можно ли идти или нет. Теперь скрытый слой у нас будет иметь функцию активации ReLU. Для обратного распространения сразу реализуем функцию производной ReLU от взвешенной суммы.

import numpy as np

data = np.array([
    [1,0,1],
    [1,1,1],
    [0,0,1],
    [0,1,1]
])

def relu(x):
    return (x > 0) * x

def relu2deriv(output):
    return output > 0

alpha = 0.2
size = 3 #количество нейронов в нашем скрытом слое
real_pred = np.array([1,1,0,0])
weight_1 = 2 * np.random.random((3, size)) - 1
weight_2 = 2 * np.random.random((size)) - 1 

Важно, что random.random заполняет матрицу значениями от 0 до 1, я же увеличил этот диапазон до 2, а затем центрировал. В итоге матрицу заполнило числами в диапазоне [-1,+1].

Далее реализуем обратное распространение и градиентный спуск

for i in range(60):
    layer_2_sse = 0
    for z in range(len(data)):
        layer_0 = data[z]
        layer_1 = relu(np.dot(layer_0, weight_1))
        layer_2 = np.dot(layer_1, weight_2)
        layer_2_error = layer_2 - real_pred[z]
        layer_2_sse += 0.5*(layer_2_error ** 2)
        layer_2_delta = layer_2_error
        layer_1_delta = layer_2_delta * weight_2 * relu2deriv(layer_1)
        weight_2 -= alpha * layer_1 * layer_2_delta
        weight_1 -= alpha * np.outer(layer_0, layer_1_delta)
    if i % 9 == 0:
        print(f"Iteration: {i} SSE: {layer_2_sse}")

Тут всё достаточно очевидно, используются формулы, которые я объяснял в посте про обратное распространение. Обратите внимание только на знак, тут используется минус, так как ошибка = предсказанное значение – реальное значение. А в посте было наоборот.

Протестируем нашу модель на новом примере

print("Тестируем на новом наборе [1,0,0], где модель должна выдать 1")
ans = relu(np.array([1,0,0]).dot(weight_1)).dot(weight_2)
print(f"Модель дала ответ:  {1 if ans > 0.7 else 0}")
Тестирование модели на новом образце

Тестирование модели на новом образце

Из скриншота видно, что модель справилась.

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

Почему возникает переобучение?

Начнём с повторения, что такое переобучение и когда встречается.

Переобучение встречается, когда используется слишком малый индуктивный сдвиг (предложение о функции, то есть даём большую свободу в выборе функции нашему алгоритму МО или нейронной сети , чтобы максимально соответствовать набору данных и больше акцентирует внимание на наборе данных). Из-за переобучения модель слишком сильно акцентирует на данный набор данных, может захватывать шумы данных, ошибки в данных, а также получиться слишком сложной.

Что такое регуляризация?

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

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

Ранняя остановка

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

Прореживание (Дропаут)

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

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

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

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

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

Пакетный градиентный спуск

Пакетный градиентный спуск корректирует веса не после каждого примера (стохастический) или всего набора данных (полный), а после просмотра пакета с указанным числом примеров (обычно от 8 до 256).

Этот метод основан на усреднении корректирующих воздействий на весовые коэффициенты в процессе обучения.

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

Рассмотрим на практике методы регуляризации (пакетный градиентный спуск и дропаут). Показывать буду на примере нейронный сети для распознавания изображения чисел (датасет mnist) с двумя слоями, скрытый слой состоит из 100 нейронов с функцией активации tanh, а выходной слой из 10 нейронов с функцией активации softmax.

Начнём с обучающего и тестового набора данных

Первым делом мы для одинаковой инициализации весов при каждом запуске вызываем np.random.seed(1).

В качестве набора данных я использую изображения из датасета mnist. Метод mnist.load_data() возвращает кортеж с двумя двухэлеметными кортежами. В x_train и x_test изображения представлены в виде трёхмерного тензора. Обучающий набор состоит из 60000 изображений по 28 пикселей на строку и столбец. А тестовый тоже самое но 10000.

Поэтому для удобства мы меняем размерность на обычную матрицу или двухмерный тензор, где будет 1000 строк и 28*28 столбцов.

Также создаём две матрицы 1000 на 10, где в нужном месте будет стоять 1 (то есть если в 5 строке и в 6 столбце стоит 1, то 6 картинка содержит цифру 7). Делим значения на 255, чтобы ужать в диапазон яркости от 0 до 1.

import numpy as np 
from keras.datasets import mnist
np.random.seed(1)

(x_train,y_train),(x_test,y_test) = mnist.load_data()
images, labels = (x_train[0:1000].reshape(1000,28*28)/255, y_train[0:1000])
one_hot_labels = np.zeros((len(labels),10))
for i,j in enumerate(labels):
    one_hot_labels[i][j] = 1
labels = one_hot_labels
x_test = x_test[0:1000].reshape(1000,28*28)/255
test_labels = np.zeros((len(y_test),10))
for i,j in enumerate(y_test):
    test_labels[i][j] = 1

Реализуем функции активации

def tanh(x): return np.tanh(x)

def tanh2deriv(x):
    return 1 - x**2

def softmax(x):
    temp = np.exp(x)
    return temp/np.sum(temp, axis=1, keepdims=True)

Тут всё очевидно, просто реализуем известные функции и их производные, производная softmax нам не нужна, так как она сократится потом. Об этом ниже.

Зададим начальные значения

Здесь мы зададим начальные гиперпараметры для нашей сети.

alpha, iteration, neirons = (0.07,300,100)
batch_size = 100
image_size, labels_num = 784, 10

weight_1 = 0.02*np.random.random((image_size, neirons)) - 0.01
weight_2 = 0.2*np.random.random((neirons,labels_num)) - 0.1

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

Обучение и тестирование

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

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

Первый слой считает взвешенную сумму сразу для всех примеров, применяет tanh и записывает в матрицу 100 на 100, где строки это примеры, а столбцы — вывод нейрона для этого примера.

Далее применяем дропаут, отключая случайную половину нейронов, не забывая усилить сигнал (сохранить математическое ожидание). И передаём матрицу 100 на 100 на вход второго слоя, применяем функцию softmax и получаем матрицу 100 на 10. Где строки это примеры, а в столбцах будет стоять число и чем оно больше тем вероятнее, что именно эта цифра на картинке (то есть если в строке 3 и столбце 6 стоит число близкое к 1, то на 4 картинке нарисована 7).

После этого мы считаем количество правильно отгаданных изображений, сравнивая в каком индексе у нас стоит наибольшее число. Тут важное отличие от прошлых наших сетей, я не прописываю в коде расчёт ошибки, потому что здесь уже не простая среднеквадратическая ошибка (SSE), а я использую перекрёстную энтропию (о ней во второй части). Она нам облегчит производную для функции softmax, производная перекрёстной энтропии по входам функции softmax это просто истина – предсказание, как в SSE вообще без функций активации на выходном слое.

#прямое распространение 

for j in range(iteration):
    correct_cnt = 0
    for i in range(int(len(images)/batch_size)):
        start, end = ((i*batch_size),((i+1)*batch_size))
        layer_0 = images[start:end]
        layer_1 = tanh(np.dot(layer_0,weight_1))
        dropout = np.random.randint(2,size=layer_1.shape)
        layer_1 *= dropout*2
        layer_2 = softmax(np.dot(layer_1,weight_2))

        for k in range(batch_size):
            correct_cnt += int(np.argmax(layer_2[k:k+1]) == np.argmax(labels[start+k:start+k+1]))

#обратное распространение 

        layer_2_delta = (labels[start:end] - layer_2)/batch_size
        layer_1_delta = np.dot(layer_2_delta,weight_2.T)*tanh2deriv(layer_1)
        layer_1_delta *= dropout
        weight_2 += alpha*layer_1.T.dot(layer_2_delta)
        weight_1 += alpha*layer_0.T.dot(layer_1_delta)

    test_correct_cnt = 0
    for i in range(len(x_test)):
        layer_0 = x_test[i:i+1]
        layer_1 = tanh(layer_0.dot(weight_1))
        layer_2 = softmax(np.dot(layer_1,weight_2))
        test_correct_cnt += int(np.argmax(layer_2) == np.argmax(test_labels[i:i+1]))

    if(j %10 == 0):
        print(f"iteration: {j} Test_acc: {test_correct_cnt/float(len(x_test))} Train_acc: {correct_cnt/float(len(images))}")

Теперь переходим к ещё более сложному этапу. Обратное распространение начинается у нас с расчёта дельты для выходного слоя. Мы получаем вектор дельт, полученных для каждого примера и делим их значение на количество примеров в пакете (то есть в итоге у нас матрица 100 на 10, где строки это примеры, а столбцы это дельты для соответствующего нейрона). Эту дельту мы получили очень просто благодаря тому, что производная перекрёстной энтропии к взвешенной суммы подаваемой на вход softmax — это просто истина – результат.

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

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

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

В конце проводим тестирование нашей модели после каждой итерации (прохода по всему набору изображений).

 Тестирование модели

Тестирование модели

Теперь поговорим про свёрточные нейронные сети (CNN), которые являются результатом адаптации структуры нейронной сети к определённым характеристикам данных.

Что такое свёрточная нейронная сеть?

Свёрточные нейронные сети были созданы для распознавания образов. Основная задача этой архитектуры состояла создании сети, в которой начальный слой извлекает локальные признаки,а последующие слои объединяют их в признаки более высокого уровня.

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

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

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

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

То, что функция, реализованная нейроном, определяется его весами, означает: два нейрона с одинаковым сочетанием весов реализуют одну и ту же функцию (механизм обнаружения признаков).

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

Основные операции для CNN

Основные этапы работы свёрточной нейронной сети

Основные этапы работы свёрточной нейронной сети

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

Матрица размером 6×6 в левой части представляет изображение, которое подаётся на вход сети. Справа от ввода находится матрица размером 4×4, представляющая слой нейронов, которые вместе прочёсывают изображение в поисках конкретного локального признака. Каждый из них соединен со своим рецептивным полем изображения размером 3х3, все они применяют к своему вводу одну и ту же матрицу весов 3х3.

Здесь рецептивные поля пересекаются, потому что имеют сдвиг равный единице, сдвиг является тут гиперпараметром (его выбирает сам человек).

По сути реализуется операция свёртки, мы последовательно применяем одну и ту же матрицу весов (матрицу свёртки или ядро) к различным частям входных данных. Результатом скалярного умножения матриц является число, которое мы записываем в одну из ячеек карты признаков.

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

После заполнения карты признаков применяется нелинейная функция активации к её элементам.

Операция пуллинга (объединения)

После того как свёрточный слой нашёл признаки (например, края, углы, текстуры), приступаем к пуллингу.

Что делает пуллинг?

  • Уменьшает размерность карты признаков.

  • Делает сеть устойчивее к небольшим смещениям и искажениям изображения.

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

По сути мы делим нашу карту признаков на квадраты меньшего размера (это называется окном пуллинга). Для каждого квадрата берём максимальное значение (Max Pooling) или среднее значение (Average Pooling) и записываем его в новую, уменьшенную карту.

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

Все нейроны плотного слоя принимают на вход выходы всех нейронов предыдущего слоя и именно плотный слой генерирует окончательный ответ.

Пишите в комментариях, если нашли ошибки или стоит разобрать какой-то момент подробнее. Стоит ли делать 2 часть?

Автор: PXI

Источник