Рассказываем, как за несколько шагов создать простую нейронную сеть и научить её узнавать известных предпринимателей на фотографиях.
Шаг 0. Разбираемся, как устроены нейронные сети
Проще всего разобраться с принципами работы нейронных сетей можно на примере Teachable Machine — образовательного проекта Google.
В качестве входящих данных — то, что нужно обработать нейронной сети — в Teachable Machine используется изображение с камеры ноутбука. В качестве выходных данных — то, что должна сделать нейросеть после обработки входящих данных — можно использовать гифку или звук.
Например, можно научить Teachable Machine при поднятой вверх ладони говорить «Hi». При поднятом вверх большом пальце — «Cool», а при удивленном лице с открытым ртом — «Wow».
Для начала нужно обучить нейросеть. Для этого поднимаем ладонь и нажимаем на кнопку «Train Green» — сервис делает несколько десятков снимков, чтобы найти на изображениях закономерность. Набор таких снимков принято называть «датасетом».
Теперь остается выбрать действие, которое нужно вызывать при распознании образа — произнести фразу, показать GIF или проиграть звук. Аналогично обучаем нейронную сеть распознавать удивленное лицо и большой палец.
Как только нейросеть обучена, её можно использовать. Teachable Machine показывает коэффициент «уверенности» — насколько система «уверена», что ей показывают один из навыков.
Шаг 1. Готовим компьютер к работе с нейронной сетью
Теперь сделаем свою нейронную сеть, которая при отправке изображения будет сообщать о том, что изображено на картинке. Сначала научим нейронную сеть распознавать цветы на картинке: ромашку, подсолнух, одуванчик, тюльпан или розу.
Для создания собственной нейронной сети понадобится Python — один из наиболее минималистичных и распространенных языков программирования, и TensorFlow — открытая библиотека Google для создания и тренировки нейронных сетей.
Устанавливаем Python
Если у вас Windows: скачиваем установщик с официального сайта Python и запускаем его. При установке нужно поставить галочку «Add Python to PATH».
На macOS Python можно установить сразу через Terminal:
brew install python
Для работы с нейронной сетью подойдет Python 2.7 или более старшая версия.
Устанавливаем виртуальное окружение
Открываем командную строку на Windows или Terminal на macOS и последовательно вводим несколько команд:
pip install —upgrade virtualenv
virtualenv —system-site-packages Название
source Название/bin/activate
На компьютер будет установлен инструмент для запуска программ в виртуальном окружении. Он позволит устанавливать и запускать все библиотеки и приложения внутри одной папки — в команде она обозначена как «Название».
Устанавливаем TensorFlow
Вводим команду:
pip install tensorflow
Всё, библиотека TensorFlow установлена в выбранную папку. На macOS она находится по адресу Macintosh HD/Users/Имя_пользователя/, на Windows — в корне C://.
Можно проверить работоспособность библиотеки последовательно вводя команды:
python
import tensorflow as tf
hello = tf.constant(‘Hello, TensorFlow’)
sess = tf.Session()
print(sess.run(hello))
Если установка прошла успешно, то на экране появится фраза «Hello, Tensorflow».
Шаг 2. Добавляем классификатор
Классификатор — это инструмент, который позволяет методам машинного обучения понимать, к чему относится неизвестный объект. Например, классификатор поможет понять, где на картинке растение, и что это за цветок.
Открываем страницу «Tensorflow for poets» на Github, нажимаем на кнопку «Clone or download» и скачиваем классификатор в формате ZIP-файла.
Затем распаковываем архив в созданную на втором шаге папку.
Шаг 3. Добавляем набор данных
Набор данных нужен для обучения нейронной сети. Это входные данные, на основе которых нейронная сеть научится понимать, какой цветок расположен на картинке.
Сначала скачиваем набор данных (датасет) Google с цветами. В нашем примере — это набор небольших фотографий, отсортированный по папкам с их названиями.
Содержимое архива нужно распаковать в папку /tf_files классификатора.
Шаг 4. Переобучаем модель
Теперь нужно запустить обучение нейронной сети, чтобы она проанализировала картинки из датасета и поняла при помощи классификатора, как и какой тип цветка выглядит.
Переходим в папку с классификатором
Открываем командную строку и вводим команду, чтобы перейти в папку с классификатором.
Windows:
cd C://Название/
macOS:
cd Название
Запускаем процесс обучения
python scripts/retrain.py —output_graph=tf_files/retrained_graph.pb —output_labels=tf_files/retrained_labels.txt —image_dir=tf_files/flower_photos
Что указано в команде:
- retrain.py — название Python-скрипта, который отвечает за запуск процесса обучения нейронной сети.
- output_graph — создаёт новый файл с графом данных. Он и будет использоваться для определения того, что находится на картинке.
- output_labels — создание нового файла с метками. В нашем примере это ромашки, подсолнухи, одуванчики, тюльпаны или розы.
- image_dir — путь к папке, в которой находятся изображения с цветами.
Программа начнет создавать текстовые файлы bottleneck — это специальные текстовые файлы с компактной информацией об изображении. Они помогают классификатору быстрее определять подходящую картинку.
Весь ход обучения занимает около 4000 шагов. Время работы может занять несколько десятков минут — в зависимости от мощности процессора.
После завершения анализа нейросеть сможет распознавать на любой картинке ромашки, подсолнухи, одуванчики, тюльпаны и розы.
Перед тестированием нейросети нужно открыть файл label_image.py, находящийся в папке scripts в любом текстовом редакторе и заменить значения в строках:
input_height = 299
input_width = 299
input_mean = 0
input_std = 255
input_layer = «Mul»
Шаг 5. Тестирование
Выберите любое изображение цветка, которое нужно проанализировать, и поместите его в папку с нейронной сетью. Назовите файл image.jpg.
Для запуска анализа нужно ввести команду:
python scripts/label_image.py —image image.jpg
Нейросеть проверит картинку на соответствие одному из лейблов и выдаст результат.
Например:
Это значит, что с вероятностью 72% на картинке изображена роза.
Шаг 6. Учим нейронную сеть распознавать предпринимателей
Теперь можно расширить возможности нейронной сети — научить её распознавать на картинке не только цветы, но и известных предпринимателей. Например, Элона Маска и Марка Цукерберга.
Для этого нужно добавить новые изображения в датасет и переобучить нейросеть.
Собираем собственный датасет
Для создания датасета с фотографиями предпринимателей можно воспользоваться поиском по картинкам Google и расширением для Chrome, которое сохраняет все картинки на странице.
Папку с изображениями Элона Маска нужно поместить в tf_filesflower_photosmusk. Аналогично все изображения с основателем Facebook — в папку tf_filesflower_photoszuckerberg.
Чем больше фотографий будет в папках, тем точнее нейронная сеть распознает на ней предпринимателя.
Переобучаем и проверяем
Для переобучения и запуска нейронной сети используем те же команды, что и в шагах 4 и 5.
python scripts/retrain.py —output_graph=tf_files/retrained_graph.pb —output_labels=tf_files/retrained_labels.txt —image_dir=tf_files/flower_photos
python scripts/label_image.py —image image.jpg
Шаг 7. «Разгоняем» нейронную сеть
Чтобы процесс обучения не занимал каждый раз много времени, нейросеть лучше всего запускать на сервере с GPU — он спроектирован специально для таких задач.
Процесс запуска и обучения нейронной сети на сервере похож на аналогичный процесс на компьютере.
Создание сервера с Ubuntu
Нам понадобится сервер с операционной системой Ubuntu. Её можно установить самостоятельно, либо — если арендован сервер Selectel — через техподдержку компании.
Установка Python
sudo apt-get install python3-pip python3-dev
Установка TensorFlow
pip3 install tensorflow-gpu
Скачиваем классификатор и набор данных
Аналогично шагам 2 и 3 на компьютере, только архивы необходимо загрузить сразу на сервер.
Переобучаем модель
python3 scripts/retrain.py —output_graph=tf_files/retrained_graph.pb —output_labels=tf_files/retrained_labels.txt —image_dir=tf_files/flower_photos
Тестируем нейросеть
python scripts/label_image.py —image image.jpg
В этой статье поговорим о том, как создавать нейросети и в качестве примера рассмотрим, как сделать нейронную сеть прямого распространения с нуля. Для реализации поставленной задачи воспользуемся языком программирования C#.
Только ленивый не слышал сегодня о существовании и разработке нейронных сетей и такой сфере, как машинное обучение. Для некоторых создание нейросети кажется чем-то очень запутанным, однако на самом деле они создаются не так уж и сложно. Как же их делают? Давайте попробуем самостоятельно создать нейросеть прямого распространения, которую еще называют многослойным перцептроном. В процессе работы будем использовать лишь циклы, массивы и условные операторы. Что означает этот набор данных? Только то, что нам подойдет любой язык программирования, поддерживающий вышеперечисленные возможности. Если же у языка есть библиотеки для векторных и матричных вычислений (вспоминаем NumPy в Python), то реализация с их помощью займет совсем немного времени. Но мы не ищем легких путей и воспользуемся C#, причем полученный код по своей сути будет почти аналогичным и для прочих языков программирования.
Что же такое нейронная сеть?
Под искусственной нейронной сетью (ИНС) понимают математическую модель (включая ее программное либо аппаратное воплощение), которая построена и работает по принципу функционирования биологических нейросетей — речь идет о нейронных сетях нервных клеток живых организмов.
Говоря проще, ИНС можно назвать неким «черным ящиком», превращающим входные данные в выходные данные. Если же посмотреть на это с точки зрения математики, то речь идет о том, чтобы отобразить пространство входных X-признаков в пространство выходных Y-признаков: X → Y. Таким образом, нам надо найти некую F-функцию, которая сможет выполнить данное преобразование. На первом этапе этой информации достаточно в качестве основы.
Какую роль играет искусственный нейрон?
В нашей статье мы не будем вдаваться в лирику и рассказывать об устройстве биологического нейрона в контексте его связи с искусственной моделью. Лучше сразу перейдем к делу.
Искусственный нейрон представляет собой взвешенную сумму векторных значений входных элементов. Эта сумма передается на нелинейную функцию активации f:
Но об активации поговорим после, т. к. сейчас стоит задача узнать, каким образом вместо одного выходного значения можно получить n-значений.
Нейрослой
Один нейрон может превратить в одну точку входной вектор, но по условию мы желаем получить несколько точек, т. к. выходное Y способно иметь произвольную размерность, которая определяется лишь ситуацией (один выход для XOR, десять выходов, чтобы определить принадлежность к одному из десяти классов, и так далее). Каким же образом получить n точек? На деле все просто: для получения n выходных значений, надо задействовать не один нейрон, а n. В результате для каждого элемента выходного Y будет использовано n разных взвешенных сумм от X. В итоге мы придем к следующему соотношению:
Давайте внимательно посмотрим на него. Вышенаписанная формула — это не что иное, как определение умножения матрицы на вектор. И в самом деле, если мы возьмем матрицу W размера n на m и выполним ее умножение на X размерности m, то мы получим другое векторное значение n-размерности, то есть как раз то, что надо.
Таким образом, мы можем записать похожее выражение в более удобной матричной форме:
Но полученный вектор представляет собой неактивированное состояние (промежуточное, невыходное) всех нейронов, а для того, чтобы нам получить выходное значение, нужно каждое неактивированное значение подать на вход вышеупомянутой функции активации. Итогом ее применения и станет выходное значение слоя.
Ниже показан пример нейронной сети, имеющей 2 входа, 5 нейронов и 1 выход:
Последовательность нейрослоев часто применяют для более глубокого обучения нейронной сети и большей формализации имеющихся данных. Именно поэтому, чтобы получить итоговый выходной вектор, нужно проделать вышеописанную операцию пару раз подряд по направлению от одного слоя к другому. В результате для 1-го слоя входным вектором будет являться X, а для последующих входом будет выход предыдущего слоя. То есть нейронная сеть может выглядеть следующим образом:
Функция активации
Речь идет о функции, добавляющей в нейронную сеть нелинейность. В результате нейроны смогут относительно точно сымитировать любую функцию. Широко распространены следующие функции активации:
Каждая из них имеет свои особенности.
Пишем код
Теперь мы знаем достаточно, чтобы создать простую нейронную сеть. Чтобы сделать то, что задумали, нам потребуются:
- Вектор.
- Матрица (каждый слой включает в себя матрицу весовых коэффициентов).
- Нейронная сеть.
Начнем с вектора. Создавать его можно:
- из количества элементов;
- из перечисления вещественных чисел.
Также мы можем получать и менять значения по индексу i.
Пишем код:
Теперь очередь матрицы. Ее можно создавать из числа строк и столбцов, а также генератора случайных чисел, причем есть возможность получать и менять значения по индексам i и j.
А вот и сама нейронная сеть:
Как будем обучать?
Пусть у нас уже есть нейронная сеть, но ведь ее ответы являются случайными, то есть наша нейросеть не обучена. Сейчас она способна лишь по входному вектору input выдавать случайный ответ, но нам нужны ответы, которые удовлетворяют конкретной поставленной задаче. Дабы этого достичь, сеть надо обучить. Здесь потребуется база тренировочных примеров и множество пар X — Y, на которых и будет происходить обучение, причем с использованием известного алгоритма обратного распространения ошибки.
Некоторые особенности работы этого алгоритма:
- на вход сети подается обучающий пример (1 входной вектор);
- сигнал распространяется по нейросети вперед (получаем выход сети);
- вычисляется ошибка (это разница между получившимся и ожидаемым векторами);
- ошибка распространяется на предыдущие слои;
- происходит обновление весовых коэффициентов в целях уменьшения ошибки.
Вот как выглядит алгоритм обучения:
Переходим к обучению
Для обратного распространения ошибки нужно знать значения выходов и входов, а также значения производных функции активации нейросети, причем послойно, следовательно, нужно создать структуру LayerT, где будут три векторных значения:
- x — вход слоя,
- z — выход,
- df — производная функции активации.
Для каждого слоя нам потребуются векторы дельт, в результате чего надо будет добавить в класс еще и их. В итоге класс будет выглядеть следующим образом:
Несколько слов об обратном распространении ошибки
В качестве функции оценки нейросети E(W) мы берем среднее квадратичное отклонение:
Дабы найти значение ошибки E, надо найти сумму квадратов разности векторных значений, которые были выданы нейронной сетью в виде ответа, а также вектора, который ожидается увидеть при обучении. Еще надо будет найти дельту каждого слоя и учесть, что для последнего слоя дельта будет равняться векторной разности фактического и ожидаемого результатов, покомпонентно умноженной на векторное значение производных последнего слоя:
Когда мы узнаем дельту последнего слоя, мы сможем найти дельты и всех предыдущих слоев. Чтобы это сделать, нужно будет лишь перемножить для текущего слоя транспонированную матрицу с дельтой, а потом перемножить результат с вектором производных функции активации предыдущего слоя:
Смотрим реализацию в коде:
Обновление весовых коэффициентов
Для уменьшения ошибки нейронной сети надо поменять весовые коэффициенты, причем послойно. Каким же образом это осуществить? Ничего сложного в этом нет: надо воспользоваться методом градиентного спуска. То есть нам надо рассчитать градиент по весам и сделать шаг от полученного градиента в отрицательную сторону. Давайте вспомним, что на этапе прямого распространения мы запоминали входные сигналы, а во время обратного распространения ошибки вычисляли дельты, причем послойно. Как раз ими и надо воспользоваться в целях нахождения градиента. Градиент по весам будет равняться не по компонентному перемножению дельт и входного вектора. Дабы обновить весовые коэффициенты, снизив таким образом ошибку нейросети, нужно просто вычесть из матрицы весов итог перемножения входных векторов и дельт, помноженный на скорость обучения. Все вышеперечисленное можно записать в следующем виде:
Вот оно, обучение!
Теперь мы имеем все нужные нам методы, поэтому остается лишь всё это вместе соединить, сформировав единый метод обучения.
Наша сеть готова, но мы пока ее еще ничему не научили. Сейчас это исправим.
Тренировка нейронной сети. Функции XOR
Функция XOR интересна тем, что ее нельзя получить одним нейроном:
Но ее легко получить путем увеличения количества нейронов. Давайте попробуем реализовать обучение с тремя нейронами в скрытом слое и одним выходным (выход ведь у нас только один). Чтобы все получилось, создадим массив X и Y, имеющий обучающие данные и саму нейронную сеть:
Теперь запускаем обучение с параметрами ниже:
- скорость обучения — 0.5,
- количество эпох — 100000,
- значение ошибки — 1e-7.
Выполнив обучение, посмотрим итоги, для чего надо будет сделать прямой проход для всех элементов:
В итоге вывод будет следующим:
Результаты
Мы написали нейронную сеть прямого распространения и не только написали, но и обучили ее функции XOR. Также была обеспечена универсальность, поэтому эту нейросеть можно обучать на любых данных — потребуется лишь:
- подготовить 2 векторных обучающих массива векторов X и Y,
- подобрать параметры,
- запустить само обучение,
- наблюдать за процессом.
Однако помните, что если используется сигмоидальная функция активации, выходные числа не будут больше единицы, что означает, что для обучения данным, которые существенно больше единицы, нужно будет нормировать их, приводя к отрезку [0, 1].
Надеемся, что материал был вам полезен и теперь вы знаете, как сделать нейросеть, и какие нюансы разработки стоит учитывать. Если же интересуют более продвинутые знания, обратите внимание на курсы, которые разработала команда Otus:
По материалам: https://programforyou.ru/poleznoe/pishem-neuroset-pryamogo-rasprostraneniya.
Все курсы > Вводный курс > Занятие 21
В завершающей лекции вводного курса ML мы изучим основы нейронных сетей (neural network), более сложных алгоритмов машинного обучения.
Алгоритмы нейронных сетей принято относить к области глубокого обучения (deep learning). Все изученные нами ранее алгоритмы относятся к так называемому традиционному машинному обучению (traditional machine learning).
Прежде чем перейти к этому занятию, настоятельно рекомендую пройти предыдущие уроки вводного курса.
Смысл, структура и принцип работы
Смысл алгоритма нейронной сети такой же, как и у классических алгоритмов. Мы также имеем набор данных и цель, которой хотим добиться, обучив наш алгоритм (например, предсказать число или отнести объект к определенному классу).
Отличие нейросети от других алгоритмов заключается в ее структуре.
Как мы видим, нейронная сеть состоит из нейронов, сгруппированных в слои (layers), у нее есть входной слой (input layer), один или несколько скрытых слоев (hidden layers) и выходной слой (output layer). Каждый нейрон связан с нейронами предыдущего слоя через определенные веса.
Количество слоев и нейронов не ограничено. Эта особенность позволяет нейронной сети моделировать очень сложные закономерности, с которыми бы не справились, например, линейные модели.
Функционирует нейросеть следующим образом.
На первом этапе данные подаются в нейроны входного слоя (x и y) и умножаются на соответствующие веса (w1, w2, w3, w4). Полученные произведения складываются. К результату прибавляется смещение (bias, в данном случае b1 и b2).
$$ w_{1}cdot x + w_{3}cdot y + b_{1} $$
$$ w_{2}cdot x + w_{4}cdot y + b_{2} $$
Получившаяся сумма подаётся в функцию активации (activation function) для ограничения диапазона и стабилизации результата. Этот результат записывается в нейроны скрытого слоя (h1 и h2).
$$ h_{1} = actfun(w_{1}cdot x + w_{3}cdot y + b_{1}) $$
$$ h_{2} = actfun(w_{2}cdot x + w_{4}cdot y + b_{2}) $$
На втором этапе процесс повторяется для нейронов скрытого слоя (h1 и h2), весов (w5 и w6) и смещения (b3) до получения конечного результата (r).
$$ r = actfun(w_{5}cdot h_{1} + w_{6}cdot h_{2} + b_{3}) $$
Описанная выше нейронная сеть называется персептроном (perceptron). Эта модель стремится повторить восприятие информации человеческим мозгом и учитывает три этапа такого процесса:
- Восприятие информации через сенсоры (входной слой)
- Создание ассоциаций (скрытый слой)
- Реакцию (выходной слой)
Основы нейронных сетей на простом примере
Приведем пример очень простой нейронной сети, которая на входе получает рост и вес человека, а на выходе предсказывает пол. Скрытый слой в данном случае мы использовать не будем.
В качестве функции активации мы возьмём сигмоиду. Ее часто используют в задачах бинарной (состоящей из двух классов) классификации. Приведем формулу.
$$ f(x) = frac{mathrm{1} }{mathrm{1} + e^{-x}} $$
График сигмоиды выглядит следующим образом.
Эта функция преобразует любые значения в диапазон (или вероятность) от 0 до 1. В случае задачи классификации, если результат (вероятность) близок к нулю, мы отнесем наблюдение к одному классу, если к единице, то к другому. Граница двух классов пройдет на уровне 0,5.
Общее уравнение нейросети выглядит следующим образом.
$$ r = sigmoid(w_{1}cdot weight + w_{2}cdot height + bias) $$
Теперь предположим, что у нас есть следующие данные и параметры нейросети.
Откроем ноутбук к этому занятию⧉
# даны вес и рост трех человек # единицей мы обозначим мужской пол, а нулем — женский. data = { ‘Иван’: [84, 180, 1], ‘Мария’: [57, 165, 0], ‘Анна’: [62, 170, 0] } |
# и даны следующие веса и смещение w1, w2, b = 0.3, 0.1, —39 |
Пропустим первое наблюдение через нашу нейросеть. Следуя описанному выше процессу, вначале умножим данные на соответствующие веса и прибавим смещение.
r = w1 * data[‘Иван’][0] + w2 * data[‘Иван’][1] + b |
Теперь к полученному результату (r) применим сигмоиду.
np.round(1 / (1 + np.exp(—r)), 3) |
Результат близок к единице, значит пол мужской. Модель сделала верный прогноз. Повторим эти вычисления для каждого из наблюдений.
# пройдемся по ключам и значениям нашего словаря с помощью метода .items() for k, v in data.items(): # вначале умножим каждую строчку данных на веса и прибавим смещение r1 = w1 * v[0] + w2 * v[1] + b # затем применим сигмоиду r2 = 1 / (1 + np.exp(—r1)) # если результат больше 0,5, модель предскажет мужской пол if r2 > 0.5: print(k, np.round(r2, 3), ‘male’) # в противном случае, женский else: print(k, np.round(r2, 3), ‘female’) |
Иван 0.985 male Мария 0.004 female Анна 0.032 female |
Как мы видим, модель отработала верно.
Обучение нейронной сети
В примере выше был описан первый этап работы нейронной сети, называемый прямым распространением (forward propagation).
И кажется, что этого достаточно. Модель справилась с поставленной задачей. Однако, обратите внимание, веса были подобраны заранее и никаких дополнительных действий от нас не потребовалось.
В реальности начальные веса выбираются случайно и отклонение истинного результата от расчетного (т.е. ошибка) довольно велико.
Как и с обычными алгоритмами ML, для построения модели, нам нужно подобрать идеальные веса или заняться оптимизацией. Применительно к нейронным сетям этот процесс называется обратным распространением (back propagation).
В данном случае мы как бы двигаемся в обратную сторону и, уже зная результат (и уровень ошибки), с учётом имеющихся данных рассчитываем, как нам нужно изменить веса и смещения, чтобы уровень ошибки снизился.
Для того чтобы математически описать процесс оптимизации, нам не хватает знаний математического анализа (calculus) и, если говорить более точно, понятия производной (derivative).
Затем, уже с новыми весами, мы снова повторяем весь процесс forward propagation слева направо и снова рассчитываем ошибку. После этого мы вновь меняем веса в ходе back propagation.
Эти итерации повторяются до тех пор, пока ошибка не станет минимальной, а веса не будут подобраны идеально.
Создание нейросети в библиотеке Keras
Теперь давайте попрактикуемся в создании и обучении нейронной сети с помощью библиотеки Keras. В первую очередь установим необходимые модули и библиотеки.
# установим библиотеку tensorflow (через нее мы будем пользоваться keras) и модуль mnist !pip install tensorflow mnist |
И импортируем их.
# импортируем рукописные цифры import mnist # и библиотеку keras from tensorflow import keras |
1. Подготовка данных
Как вы вероятно уже поняли, сегодня мы снова будем использовать уже знакомый нам набор написанных от руки цифр MNIST (только на этот раз воспользуемся не библиотекой sklearn, а возьмем отдельный модуль).
В модуле MNIST содержатся чёрно-белые изображения цифр от 0 до 9 размером 28 х 28 пикселей. Каждый пиксель может принимать значения от 0 (черный) до 255 (белый).
Данные в этом модуле уже разбиты на тестовую и обучающую выборки. Посмотрим на обучающий набор данных.
# сохраним обучающую выборку и соответсвующую целевую переменную X_train = mnist.train_images() y_train = mnist.train_labels() # посмотрим на размерность print(X_train.shape) print(y_train.shape) |
Как мы видим, обучающая выборка содержит 60000 изображений и столько же значений целевой переменной. Теперь посмотрим на тестовые данные.
# сделаем то же самое с тестовыми данными X_test = mnist.test_images() y_test = mnist.test_labels() # и также посмотрим на размерность print(X_test.shape) print(y_test.shape) |
Таких изображений и целевых значений 10000.
Посмотрим на сами изображения.
# создадим пространство для четырех картинок в один ряд fig, axes = plt.subplots(1, 4, figsize = (10, 3)) # в цикле for создадим кортеж из трех объектов: id изображения (всего их будет 4), самого изображения и # того, что на нем представлено (целевой переменной) for ax, image, label in zip(axes, X_train, y_train): # на каждой итерации заполним соответствующее пространство картинкой ax.imshow(image, cmap = ‘gray’) # и укажем какой цифре соответствует изображение с помощью f форматирования ax.set_title(f‘Target: {label}’) |
Нейросети любят, когда диапазон входных значений ограничен (нормализован). В частности, мы можем преобразовать диапазон [0, 255] в диапазон от [–1, 1]. Сделать это можно по следующей формуле.
$$ x’ = 2 frac {x-min(x)}{max(x)-min(x)}-1 $$
Применим эту формулу к нашим данным.
# функция np.min() возвращает минимальное значение, # np.ptp() — разницу между максимальным и минимальным значениями (от англ. peak to peak) X_train = 2. * (X_train — np.min(X_train)) / np.ptp(X_train) — 1 X_test = 2. * (X_test — np.min(X_test)) / np.ptp(X_test) — 1 |
Посмотрим на новый диапазон.
# снова воспользуемся функцией np.ptp() np.ptp(X_train) |
Теперь нам необходимо «вытянуть» изображения и превратить массивы, содержащие три измерения, в двумерные матрицы. Мы уже делали это на занятии по компьютерному зрению.
Применим этот метод к нашим данным.
# «вытянем» (flatten) наши изображения, с помощью метода reshape # у нас будет 784 столбца (28 х 28), количество строк Питон посчитает сам (-1) X_train = X_train.reshape((—1, 784)) X_test = X_test.reshape((—1, 784)) # посмотрим на результат print(X_train.shape) print(X_test.shape) |
Посмотрим на получившиеся значения пикселей.
# выведем первое изображение [0], пиксели с 200 по 209 X_train[0][200:210] |
array([—1. , —1. , —1. , —0.61568627, 0.86666667, 0.98431373, 0.98431373, 0.98431373, 0.98431373, 0.98431373]) |
Наши данные готовы. Теперь нужно задать конфигурацию модели.
2. Конфигурация нейронной сети
Существует множество различных архитектур нейронных сетей. Пока что мы познакомились с персептроном или в более общем смысле нейросетями прямого распространения (Feed Forward Neural Network, FFNN), в которых данные (сигнал) поступают строго от входного слоя к выходному.
Такую же сеть мы и будем использовать для решения поставленной задачи. В частности, на входе мы будем одновременно подавать 784 значения, которые затем будут проходить через два скрытых слоя по 64 нейрона каждый и поступать в выходной слой из 10 нейронов (по одному для каждой из цифр или классов).
В первую очередь воспользуемся классом Sequential библиотеки Keras, который укажет, что мы задаём последовательно связанные между собой слои.
# импортируем класс Sequential from tensorflow.keras.models import Sequential # и создадим объект этого класса model = Sequential() |
Далее нам нужно прописать сами слои и связи между нейронами.
Тип слоя Dense, который мы будем использовать, получает данные со всех нейронов предыдущего слоя. Функцией активации для скрытых слоев будет уже известная нам сигмоида.
# импортируем класс Dense from tensorflow.keras.layers import Dense # и создадим первый скрытый слой (с указанием функции активации и размера входного слоя) model.add(Dense(64, activation = ‘sigmoid’, input_shape = (784,))) # затем второй скрытый слой model.add(Dense(64, activation = ‘sigmoid’)) # и наконец выходной слой model.add(Dense(10, activation = ‘softmax’)) |
Выходной слой будет состоять из 10 нейронов, по одному для каждого из классов (цифры от 0 до 9). В качестве функции активации будет использована новая для нас функция softmax (softmax function).
Если сигмоида подходит для бинарной классификации, то softmax применяется для задач многоклассовой классификации. Приведем формулу.
$$ text{softmax}(vec{z})_{i} = frac{e^{z_i}}{sum_{j=1}^K e^{z_i}} $$
Функция softmax на входе принимает вектор действительных чисел (z), применяет к каждому из элементов zi экспоненциальную функцию и нормализует результат через деление на сумму экспоненциальных значений каждого из элементов.
На выходе получается вероятностное распределение любого количества классов (K), причем каждое значение находится в диапазоне от 0 до 1, а сумма всех значений равна единице. Приведем пример для трех классов.
Очевидно, вероятность того, что это кошка, выше. Теперь, когда мы задали архитектуру сети, необходимо заняться ее настройками.
Работа над ошибками. Внимательный читатель безусловно обратил внимание, что вероятности на картинке не соответствуют приведенным в векторе значениям. Если подставить эти числа в формулу softmax вероятности будут иными.
z = ([1, 2, 0.5]) np.exp(z) / sum(np.exp(z)) |
array([0.2312239 , 0.62853172, 0.14024438]) |
Впрочем, алгоритм по-прежнему уверен, что речь идет о кошке.
3. Настройки
Настроек будет три:
- тип функции потерь (loss function) определяет, как мы будем считать отклонение прогнозного значения от истинного
- способ или алгоритм оптимизации этой функции (optimizer) поможет снизить потерю или ошибку и подобрать правильные веса в процессе back propagation
- метрика (metric) покажет, насколько точна наша модель
Функция потерь
В первую очередь, определимся с функцией потерь. Раньше, например, в задаче регрессии, мы использовали среднеквадратическую ошибку (MSE). Для задач классификации мы будем использовать функцию потерь, называемую перекрестной или кросс-энтропией (cross-entropy). Продолжим пример с собакой, кошкой и попугаем.
Функция перекрестной энтропии (D) показывает степень отличия прогнозного вероятностного распределения (которое мы получили на выходе функции softmax (S)) от истинного (наша целевая переменная (L)). Чем больше отличие, тем выше ошибка.
Также обратите внимание, наша целевая переменная закодирована, вместо слова «кошка» напротив соответсвующего класса стоит единица, а напротив остальных классов — нули. Такая запись называется унитарным кодом, хотя чаще используется анлийский термин one-hot encoding.
Когда мы будем обучать наш алгоритм, мы также применим эту кодировку к нашим данным. Например, если в целевой переменной содержится цифра пять, то ее запись в one-hot encoding будет следующей.
В дополнение замечу, что функция кросс-энтропии, в которой применяется one-hot encoding, называется категориальной кросс-энтропией (categorical cross-entropy).
Отлично! С тем как мы будем измерять уровень ошибки (качество обучения) нашей модели, мы определились. Теперь нужно понять, как мы эту ошибку будем минимизировать. Для этого существует несколько алгоритмов оптимизации.
Алгоритм оптимизации
Классическим алгоритмом является, так называемый, метод стохастического градиентного спуска (Stochastic Gradient Descent или SGD).
Если предположить для простоты, что наша функция потерь оптимизирует один вес исходной модели, и мы находимся изначально в точке А (с неидеальным случайным весом), то наша задача — оказаться в точке B, где ошибка (L) минимальна, а вес (w) оптимален.
Спускаться мы будем вдоль градиента, то есть по кратчайшему пути. Идею градиента проще увидеть на функции с двумя весами. Такая функция имеет уже три измерения (две независимых переменных, w1 и w2, и одну зависимую, L) и графически похожа на «холмистую местность», по которой мы будем спускаться по наиболее оптимальному маршруту.
Стохастичность (или случайность) этого алгоритма заключается в том, что мы берем не всю выборку для обновления весов модели, а лишь одно или несколько случайных наблюдений. Такой подход сильно сокращает время оптимизации.
Метрика
Остается определиться с метрикой качества. Здесь мы просто возьмём знакомую нам метрику accuracy, которая посчитает долю правильно сделанных прогнозов.
Посмотрим на используемый код.
model.compile( loss = ‘categorical_crossentropy’, optimizer = ‘sgd’, metrics = [‘accuracy’] ) |
4. Обучение модели
Теперь давайте соберём все описанные выше элементы и посмотрим на работу модели в динамике. Повторим ещё раз изученные выше шаги.
- Значения пикселей каждого изображения поступают в 784 нейрона входного слоя
- Далее они проходят через скрытые слои, где они умножаются на веса, складываются, смещаются и поступают в соответствующую функцию активации
- На выходе из функции softmax мы получаем вероятности для каждой из цифр
- После этого результат сравнивается с целевой переменной с помощью функции перекрестной энтропии (функции потерь); делается расчет ошибки
- На следующем шаге алгоритм оптимизации стремится уменьшить ошибку и соответствующим образом изменяет веса
- После этого процесс повторяется, но уже с новыми весами.
Давайте выполним все эти операции в библиотеке Keras.
# вначале импортируем функцию to_categorical, чтобы сделать one-hot encoding from tensorflow.keras.utils import to_categorical |
# обучаем модель model.fit( X_train, # указываем обучающую выборку to_categorical(y_train), # делаем one-hot encoding целевой переменной epochs = 10 # по сути, эпоха показывает сколько раз алгоритм пройдется по всем данным ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Epoch 1/10 1875/1875 [==============================] — 4s 2ms/step — loss: 2.0324 — accuracy: 0.4785 Epoch 2/10 1875/1875 [==============================] — 3s 2ms/step — loss: 1.2322 — accuracy: 0.7494 Epoch 3/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.7617 — accuracy: 0.8326 Epoch 4/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.5651 — accuracy: 0.8663 Epoch 5/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.4681 — accuracy: 0.8827 Epoch 6/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.4121 — accuracy: 0.8923 Epoch 7/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.3751 — accuracy: 0.8995 Epoch 8/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.3487 — accuracy: 0.9045 Epoch 9/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.3285 — accuracy: 0.9090 Epoch 10/10 1875/1875 [==============================] — 3s 2ms/step — loss: 0.3118 — accuracy: 0.9129 <keras.callbacks.History at 0x7f36c3f09490> |
На обучающей выборке мы добились неплохого результата, 91.29%.
5. Оценка качества модели
На этом шаге нам нужно оценить качество модели на тестовых данных.
# для оценки модели воспользуемся методом .evaluate() model.evaluate( X_test, # который применим к тестовым данным to_categorical(y_test) # не забыв закодировать целевую переменную через one-hot encoding ) |
313/313 [==============================] — 1s 1ms/step — loss: 0.2972 — accuracy: 0.9173 [0.29716429114341736, 0.9172999858856201] |
Результат «на тесте» оказался даже чуть выше, 91,73%.
6. Прогноз
Теперь давайте в качестве упражнения сделаем прогноз.
# передадим модели последние 10 изображений тестовой выборки pred = model.predict(X_test[—10:]) # посмотрим на результат для первого изображения из десяти pred[0] |
array([1.0952151e-04, 2.4856537e-04, 1.5749732e-03, 7.4032680e-03, 6.2553445e-05, 8.7646207e-05, 9.4199123e-07, 9.7065586e-01, 5.3100550e-04, 1.9325638e-02], dtype=float32) |
Работа над ошибками. На видео я говорю про первые десять изображений. Разумеется, это неверно. Срез [-10:] выводит последние десять изображений.
В переменной pred содержится массив numpy с десятью вероятностями для каждого из десяти наблюдений. Нам нужно выбрать максимальную вероятность для каждого изображения и определить ее индекс (индекс и будет искомой цифрой). Все это можно сделать с помощью функции np.argmax(). Посмотрим на примере.
Теперь применим к нашим данным.
# для кажого изображения (то есть строки, axis = 1) # выведем индекс (максимальное значение), это и будет той цифрой, которую мы прогнозируем print(np.argmax(pred, axis = 1)) # остается сравнить с целевой переменной print(y_test[—10:]) |
[7 8 9 0 1 2 3 4 5 6] [7 8 9 0 1 2 3 4 5 6] |
Для первых десяти цифр модель сделала верный прогноз.
7. Пример улучшения алгоритма
Существует множество параметров модели, которые можно настроить. В качестве примера попробуем заменить алгоритм стохастического градиентного спуска на считающийся более эффективным алгоритм adam (суть этого алгоритма выходит за рамки сегодняшней лекции).
Посмотрим на результат на обучающей и тестовой выборке.
# снова укажем настройки модели model.compile( loss = ‘categorical_crossentropy’, optimizer = ‘adam’, # однако заменим алгоритм оптимизации metrics = [‘accuracy’] ) # обучаем модель методом .fit() model.fit( X_train, # указываем обучающую выборку to_categorical(y_train), # делаем one-hot encoding целевой переменной epochs = 10 # прописываем количество эпох ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Epoch 1/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.2572 — accuracy: 0.9252 Epoch 2/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.1738 — accuracy: 0.9497 Epoch 3/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.1392 — accuracy: 0.9588 Epoch 4/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.1196 — accuracy: 0.9647 Epoch 5/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.1062 — accuracy: 0.9685 Epoch 6/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.0960 — accuracy: 0.9708 Epoch 7/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.0883 — accuracy: 0.9732 Epoch 8/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.0826 — accuracy: 0.9747 Epoch 9/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.0766 — accuracy: 0.9766 Epoch 10/10 1875/1875 [==============================] — 4s 2ms/step — loss: 0.0699 — accuracy: 0.9780 <keras.callbacks.History at 0x7f36c3d74590> |
# и оцениваем результат «на тесте» model.evaluate( X_test, to_categorical(y_test) ) |
313/313 [==============================] — 1s 1ms/step — loss: 0.1160 — accuracy: 0.9647 [0.11602973937988281, 0.9646999835968018] |
Как вы видите, с помощью одного изменения мы повысили долю правильных прогнозов до 96,47%.
Более подходящие для работы с изображениями сверточные нейронные сети (convolutional neural network, CNN) достигают свыше 99% точности на этом наборе данных, как это видно в примере⧉ на официальном сайте библиотеки Keras.
Подведем итог
На сегодняшнем занятии изучили основы нейронных сетей. В частности, мы узнали, что такое нейронная сеть, какова ее структура и алгоритм функционирования. Многие шаги, например, оценка уровня ошибки через функцию кросс-энтропии или оптимизация методом стохастического градиентного спуска, разумеется, требуют отдельного занятия. Эти уроки еще впереди.
При этом, я надеюсь, у вас сложилось целостное представление о том, что значит создать и обучить нейросеть, и какие шаги для этого требуются.
Вопросы для закрепления
Перечислите типы слоев нейронной сети
Посмотреть правильный ответ
Ответ: обычно используется входной слой, один или несколько скрытых слоев и выходной слой.
Из каких двух этапов состоит обучение нейронной сети?
Посмотреть правильный ответ
Ответ: вначале (1) при forward propagation мы пропускаем данные от входного слоя к выходному, затем, рассчитав уровень ошибки, (2) начинается обратный процесс back propagation, при котором, мы улучшаем веса исходной модели.
Для чего используются сигмоида и функция softmax в выходном слое нейронной сети в задачах классификации?
Посмотреть правильный ответ
Ответ: сигмоида используется, когда нужно предсказать один из двух классов, если классов больше двух, применяется softmax.
Ответы на вопросы
Вопрос. Что означает число 1875 в результате работы модели?
Ответ. Я планировал рассказать об этом на курсе по оптимизации, но попробую дать общие определения уже сейчас. Как я уже сказал, при оптимизации методом градиентного спуска мы можем использовать (1) все данные, (2) часть данных или (3) одно наблюдение для каждого обновления весов. Это регулируется параметром batch_size (размер партии).
- в первом случае, количество наблюдений (batch, партия) равно размеру датасета, веса не обновляются пока мы не пройдемся по всем наблюдениям, это простой градиентный спуск
- во втором случае, мы берем часть наблюдений (mini-batch, мини-партия), и когда обработаем их, то обновляем веса; после этого мы обрабатываем следующую партию
- и наконец мы можем взять только одно наблюдение и сразу после его анализа обновить веса, это классический стохастический градиентный спуск (stochastic gradient descent), параметр batch_size = 1
В чем преимущество каждого из методов? Если мы берем всю партию и по результатам ее обработки обновляем веса, то двигаемся к минимуму функции потерь наиболее плавно. Минус в том, что на расчет требуется время и вычислительные мощности.
Если берем только одно наблюдение, то считаем все быстро, но расчет минимума функции потерь менее точен.
В библиотеке Keras (и нашей нейросети) по умолчанию используется второй подход и размер партии равный 32 наблюдениям (
batch_size = 32). С учетом того, что в обучающей выборке 60000 наблюдений, разделив 60000 на 32 мы получим 1875 итераций или обновлений весов в рамках одной эпохи. Отсюда и число 1875.
Повторим, алгоритм обрабатывает 32 наблюдения, обновляет веса и после этого переходит к следующей партии (batch) из 32-х наблюдений. Обработав таким образом 60000 изображений, алгоритм заканчивает первую эпоху и начинает вторую. Размер партии и количество эпох регулируется параметрами batch_size и epochs соответственно.
Сегодня у нас нестандартный проект: будем устанавливать и запускать настоящую нейросеть у себя на компьютере.
👉 Мы пока не будем подробно разбирать тонкости работы алгоритмов и писать нейронку с нуля. Вместо этого мы используем уже готовые скрипты и алгоритмы и попробуем повторить это в домашних условиях. Вам достаточно использовать команды в той же последовательности, и вы получите тот же результат.
И ещё: нейросети — это на самом деле скучно, медленно и не очень эффектно в настройке. Мы привыкли сразу видеть классный и красивый результат, а то, что было до этого, нам обычно не показывают. Эта статья работает наоборот: долго показывает весь процесс, а финальный результат получается за пару секунд.
В этом суть нейросетей: долгая и кропотливая работа ради эффектной концовки.
Что сделаем
Мы настроим и обучим нейросеть, которая будет распознавать картинки и говорить, какой цветок мы ей показываем — розу, тюльпан или что-то другое. Мы используем цветы, потому что скачали уже готовый, собранный и размеченный набор фотографий, на котором нейронка может научиться. Если вы хотите, чтобы она научилась распознавать на фото вас или ваших друзей, нужно будет собрать другой датасет и переобучить нейронку.
Как собрать и настроить такой датасет — расскажем в другой раз.
Что понадобится
Python версии 3.8 и выше, обязательно под архитектуру x64. Если взять 32-разрядную версию, то нужная в проекте библиотека tensorflow работать не будет. Мы использовали версию 3.9.7.
Остальное установим в процессе. Главное — рабочий Python (по ссылке — как его установить).
👉 Все команды, которые есть в проекте, мы будем запускать в командной строке. Чтобы не было ошибок и затыков, лучше всего запустить её от имени администратора (в Windows) или с правами суперпользователя root (в Mac OS и Linux).
Создаём виртуальное окружение
Чтобы не раскидывать файлы, скрипты и картинки по всему компьютеру, создадим в питоне виртуальное окружение — специальный проект, который хранит все данные внутри своей папки. Он не мешает остальным проектам и не влияет на работу других программ.
Чтобы подключить себе виртуальное окружение, запускаем команду:
pip install --upgrade virtualenv
Теперь можно устанавливать окружение. Для этого придумаем ему название — мы выбрали tell-me, но вы можете выбрать любое другое:
virtualenv --system-site-packages tell-me
Запускаем окружение:
source tell-me/bin/activate (если у вас мак или линукс)
tell-mescriptsactivate (если у вас виндоус)
Эта команда создаст папку на компьютере (путь к ней можно посмотреть на предыдущем скриншоте на третьей строке, параметр «dest») и запустит в ней виртуальное окружение:
Устанавливаем tensorflow
Tensorflow — открытая библиотека для машинного обучения и работы с нейросетями. Она будет отвечать за то, чтобы наш компьютер мог запустить нейросеть и правильно с ней работать.
Для установки пишем команду:
pip install tensorflow
pip — это программа, которая отвечает в Python за скачивание, установку и обновление библиотек и вспомогательных пакетов. Это как магазин приложений Apple, только для командной строки и для разработчиков.
Чтобы убедиться, что библиотека установилась правильно и работает штатно, проверим её простым тестом.
1. Пишем команду:
python
2. Начало командной строки поменялось на >>> — это значит, питон готов к приёму своих команд. Пишем по очереди такое:
hello = tf.constant('Hello, TensorFlow')
sess = tf.compat.v1.Session()
print(sess.run(hello))
Если в ответ питон нам выдал что-то вроде ‘Hello, TensorFlow’, это значит, что мы всё сделали правильно.
Устанавливаем классификатор
Классификатор в нейросетях — это алгоритм, который смотрит на объекты и пытается понять, к какой категории их отнести. То есть классифицировать.
Задача нашего классификатора — научить нейросеть понимать, чем одни цветы отличаются от других. Если бы мы вместо цветов использовали фото зданий, нейронка бы научилась отличать барокко от роккоко и неоклассицизма.
- Качаем архив с классификатором.
- Распаковываем архив.
3.Копируем содержимое архива в папку tell-me. Если вы выбрали другое название для проекта, замените tell-me на своё название.
Добавляем фото для обучения
Скачиваем уже собранный датасет с цветами, распаковываем его и копируем в папку tell-me → tf_files.
Адаптируем скрипты под актуальную версию tensorflow
👉 На момент написания статьи актуальная версия tensorflow — 2.0. Но скрипты и алгоритмы, которые мы используем, заточены под старую версию, поэтому нужно применить немного магии автозамены:
- Переходим в каталог tell-me/scripts и находим файл retrain.py.
- Открываем его в любом редакторе кода, например Sublime Text 3.
- Нажимаем Ctrl + H или Command + H — включится режим поиска и автозамены текста.
- Первая строка (что заменить) → пишем tf. (с точкой).
- Вторая строка (на что заменить) → пишем tf.compat.v1. (тоже с точкой в конце).
- Нажимаем Replace All (Заменить всё).
- То же самое делаем в файле label-image.py.
- В том же файле label-image.py добавляем после строки 25 «import tensorflow as tf» такую строку:
tf.compat.v1.disable_eager_execution()
Благодаря этому колдунству мы заставим старый скрипт работать с новой библиотекой.
Обучаем нейросеть
- В командной строке командой cd переходим в папку tell-me (или в другую, если у вас проект называется по-другому).
- Запускаем команду:
python scripts/retrain.py
--output_graph=tf_files/retrained_graph.pb
--output_labels=tf_files/retrained_labels.txt
--image_dir=tf_files/flower_photos
Пошёл процесс обучения. В нём 4000 этапов, по времени занимает примерно 20 минут. За это время нейросеть обработает около 250 фото (это очень мало для нейросети) и научится отличать розу от ландышей:
Запускаем нейросеть
Чтобы проверить работу нашей нейросети, скачиваем любой файл с розой из интернета, кладём его в папку tell-me (или как у вас она называется) и пишем такую команду:
python scripts/label_image.py --image image.jpg
Нейросеть думает, а потом выдаёт ответ в виде процентов. В нашем случае она на 98% уверена, что это роза:
А вот как нейросеть реагирует на фото Цукерберга:
50% — что на фото тюльпан, и на 18% — что это одуванчик. А всё потому, что она умеет различать только 5 видов цветов, а не всяких там цукербергов.
Вёрстка:
Кирилл Климентьев
2. Регрессия
Следующий тип задач — это регрессия.
Суть этой задачи — получить на выходе из нейронной сети не класс, а конкретное число, например:
— Определение возраста по фото
— Прогнозирование курса акций
— Оценка стоимости недвижимости
Например, у нас есть данные об объекте недвижимости, такие как: метраж, количество комнат, тип ремонта, развитость инфраструктуры в окрестностях этого объекта, удаленность от метро и т.д.
И нашей задачей является предсказать цену на этот объект, исходя из имеющихся данных. Задачи такого плана — это задачи регрессии, т.к. на выходе мы ожидаем получить стоимость — конкретное число.
3. Прогнозирование временных рядов
Прогнозирование временных рядов — это задача, во многом схожая с регрессией.
Суть её заключается в том, что у нас есть динамический временной ряд значений, и нам нужно понять, какие значения будут идти в нем дальше.
Например, это задачи по предсказанию:
— Курсов акций, нефти, золота, биткойна
— Изменению процессов в котлах (давление, концентрация тех или иных веществ и т.д.)
— Количества трафика на сайте
— Объемов потребления электроэнергии и т.д.
Даже автопилот Tesla отчасти тоже можно отнести к задаче этого типа, ведь видеоряд следует рассматривать как временной ряд из картинок.
Возвращаясь же к примеру с оценкой процессов, происходящих в котлах, задача, по сути, является двойной.
С одной стороны, мы предсказываем временной ряд давления и концентрации тех или иных веществ, а с другой — распознаем по этому временному ряду паттерны, чтобы оценить, критичная ситуация, или нет. И здесь уже идет распознавание образов.
Именно поэтому мы сразу отметили тот факт, что не всегда есть четкая граница между разными типами задач.
4. Кластеризация
Кластеризация — это обучение, когда нет учителя.
В этой ситуации у нас есть много данных и неизвестно, какие из них к какому классу относятся, при этом есть предположение, что там есть некоторое количество классов.
Например, это:
— Выявление классов читателей при email-рассылках
— Выявление классов изображений
Типичная маркетинговая задача — эффективно вести email-рассылку. Допустим, у нас есть миллион email-адресов, и мы ведем некую рассылку.
Разумеется, люди ведут себя совершенно по-разному: кто-то открывает почти все письма, кто-то не открывает почти ничего.
Кто-то постоянно кликает по ссылкам в письмах, а кто-то просто их читает и кликает крайне редко.
Кто-то открывает письма по вечерам в выходные, а кто-то — утром в будние, и так далее.
Задача кластеризации в этом случае — это анализ всего объема данных и выделение нескольких классов подписчиков, обладающих сходными поведенческими паттернами (в рамках, каждого класса, разумеется).
Другая задача этого типа — это, например, выявление классов для изображений. Один из проектов в нашей Лаборатории — это работа с фентези-картинками.
Если передать эти изображения нейронной сети, то, после их анализа, она, к примеру «скажет» нам, что обнаружила 17 различных классов.
Допустим, один класс — это изображение с замками. Другой — это картинки с изображением человека по центру. Третий класс — это когда на картинке дракон, четвертый — это много персонажей на фоне природного ландшафта, происходит какая-нибудь битва, и т.д.
Это примеры типичных задач кластеризации.
5. Генерация
Генеративные сети (GAN) — это самый новый, недавно появившийся тип сетей, который стремительно развивается.
Если говорить кратко, то их задача – машинное творчество. Под машинным творчеством подразумевается генерация любого контента:
— Текстов (стихи, тексты песен, рассказы)
— Изображений (в том числе фотореалистичных)
— Аудио (генерация голоса, музыкальных произведений) и т.д.
Кроме того, в этот список можно добавить и задачи трансформации контента:
— раскрашивание черно-белых фильмов в цветные
— изменение сезона в видеоролике (например, трансформация окружающей среды из зимы в лето) и др.
В случае с «переделкой» сезона изменения затрагивают все важные аспекты. Например, есть видео с регистратора, в котором машина зимой едет по улицам города, вокруг лежит снег, деревья стоят голые, люди в шапках и т.д.
Параллельно с этим роликом идет другой, переделанный в лето. Там уже вместо снега зеленые газоны, деревья с листвой, люди легко одеты и т.д.
Безусловно, есть и некоторые другие типы задач, которые решают нейронные сети, но все основные мы с вами рассмотрели.
1. Самообучение
Косвенно мы уже касались этой момента выше, поэтом сейчас раскроем его чуть больше.
Суть самообучения заключается в том, что мы задаем общий алгоритм, как она будет обучаться, а дальше она сама «понимает», как некое явление или процесс устроены изнутри.
Суть самообучения заключается в том, что нейронной сети для успешной работы нужно дать правильные, подготовленные данные и прописать алгоритм, по которому она будет обучаться.
В качестве иллюстрации для этой особенности можно привести генетический алгоритм, суть которого мы рассмотрим на примере программы с летающими по экрану монитора смайликами.
Одновременно перемещалось 50 смайликов, и они могли замедляться / ускоряться и менять направление своего движения.
В алгоритме мы заложили, что если они вылетают за край экрана или попадают под клик мыши, то «умирают».
После «смерти» тут же создавался новый смайлик, причем на основании того, кто «прожил» дольше всего.
Это была вероятностная функция, которая отдавала приоритет «рождения» новых смайликов тем, кто прожил дольше всех и минимизировала вероятность появления «потомства» у тех, кто быстро улетел за пределы экрана или попался под клик мыши.
Сразу после запуска программы все смайлики начинают летать хаотично и быстро «умирают», улетая за края экрана.
Проходит буквально 1 минута, и они научаются не «умирать», улетая за правый край экрана, но улетают вниз. Еще через минуту они перестают улетать вниз. Через 5 минут они уже вообще не умирают.
Смайлики разбились на несколько групп: кто-то летал вдоль периметра экрана, кто-то топтался в центре, кто-то нарезал восьмерки, и т.д. При этом мышкой мы их еще не кликали.
Теперь мы начали кликать по ним мышкой и, естественно, они тут же умирали, потом что еще не обучались избегать курсора.
Через 5 минут было невозможно поймать ни одного из них. Как только к ним приближался курсор, смайлики делали какой-нибудь кульбит и уходили от клика — поймать их было невозможно.
Самое важное и восхитительное во всем это то, что мы не закладывали в них алгоритм, как не улетать за края экрана и уворачиваться от мышки. И если первый момент реализовать несложно, то со вторым все на порядок сложнее.
Можно было бы потратить примерно неделю времени и создать нечто подобное, но прелесть в том, что мы добились такого же или даже лучшего результата, потратив на программирование пару часов и еще несколько минут на то, чтобы произошло обучение.
Вдобавок к этому, поменяв пару строчек кода, мы могли бы довнести в их алгоритм обучения любую другую функцию. Например, если это происходит на фоне рабочего стола — не залезать на иконки, которые на нем есть.
Или мы хотим, чтобы они не сталкивались друг с другом. Или мы хотим поделить их на 2 группы, раскрашенных в разные цвета и хотим, чтобы они не сталкивались со смайликами другого цвета. И так далее.
И они обучились бы этому за те же самые 5-10 минут по тем же принципам генетических алгоритмов.
Надеюсь, что этот пример показал вам потрясающую силу всего, что связано с искусственным интеллектом и нейронными сетями.
При всем при этом важно иметь компетенцию писать такие генетические алгоритмы и уметь подавать такой сети правильные входные данные.
Так, в рассмотренном выше примере сначала никак не удавалось добиться сколь-нибудь значимого прогресса в обучении смайликов по той причине, что в качестве входных данных им подавалось расстояние до краев экрана и расстояние до курсора мыши.
В результате этого нейронной сети не хватало мощности обучиться на таком типе данных, и прогресса практически не было.
После этого в качестве входных параметров стали передаваться другие значения — единица, деленная на расстояние до края экрана и единица, деленная на расстояние до курсора.
Результат — прекрасное обучение, о котором мы только что говорили. Почему? Потому что когда они подлетали к краю экрана, этот параметр начинал зашкаливать, его изменение происходило очень быстро, и в этой комбинации нейронная сеть смогла выявить важную закономерность и обучиться.
На этом с самообучением всё, двигаемся дальше.
2. Требуется обучающая выборка
Нейронным сетям для обучения требуется обучающая выборка. Это главный минус, т.к. обычно нужно собрать много данных.
К примеру, если мы пишем распознавание лиц в классическом машинном обучении, то нам не нужна база из фотографий – достаточно нескольких фото, по которым человек сам составит алгоритм определения лица, или даже сделает это по памяти, вообще без фото.
В случае с нейронными сетями ситуация иная – нужна большая база того, на чем мы хотим обучить нейронную сеть. При этом важно не только собрать базу, но и определенным образом её предобработать.
Поэтому очень часто сбор базы производится в том числе фрилансерами или крупными бизнесами (классический пример — сбор данных компанией Google для того, чтобы показывать нам капчи).
На своем курсе мы делаем на это упор — в том числе в Лабораториях и во время подготовки дипломных работ. Важно научиться собирать эти данные руками, получив ценный опыт, поняв, как это делается правильно, и на что нужно обратить внимание.
Если вы освоите это сами, то в будущем сможете писать в том числе и правильные ТЗ для подрядчиков, получая качественные, нужные вам результаты.
3. Не требуется понимания явления человеком
Третий пункт — это огромный плюс нейронных сетей.
Можно не быть экспертом в какой-либо предметной области, но, будучи экспертом в нейронных сетях решить практически любую задачу.
Да, экспертность, безусловно влияет в плюс, т.к. мы можем лучше предобработать данные и, возможно, быстрее решить задачу, но в целом это не является критическим фактором.
Если в классическом машинном обучении не являясь экспертом в предметной области (например, атомная энергетика, машиностроение, геологические процессы и т.п.) мы вообще не сможем решить задачу, то здесь это вполне реально.
4. Значительно точнее в большинстве задач
Следующее значимое преимущество нейронных сетей — их высокая точность.
Например, аварийность автопилотов Tesla на пробег в 1 млн. км. примерно в 10 раз ниже, чем аварийность обычного водителя.
Точность предсказания медицинских диагнозов по МРТ также выше, чем у специалистов-врачей.
Нейронные сети обыгрывают людей практически в любые игры: шахматы, покер, го, компьютерные игры и т.д.
Одним словом, их точность значительно выше практически во всех задачах, что и привело к их все более и более активному использованию в самых разных сферах жизни человека, начиная с маркетинга и финансов, и заканчивая медициной, промышленностью и искусством.
5. Непонятно, как работают внутри
Однако, при всех этих возможностях у нейронных сетей есть следующий минус — непонятно, как они работают «внутри».
Мы не можем туда влезть и понять, как там все устроено, как она «мыслит». Это все скрыто в весах, в цифрах.
Более или менее «увидеть» то, что происходит внутри возможно, разве что, в сверточных сетях при работе с изображениями, но даже это не позволяет нам получить полной картины.
Типичный пример того, что нейронная сеть в чем-то похожа на черный ящик — это ситуации, когда, например идет судебный процесс, в ходе которого какие-нибудь компании обращаются в Яндекс и спрашивают, почему именно этот сайт, а не какой-то другой выводится именно на этот позиции в выдаче.
И Яндекс не может ответить на подобные вопросы, т.к. это решение принимается нейронной сетью, а не чётко прописанным алгоритмом, который можно полностью объяснить.
Ответить на такой вопрос — задача столь же нетривиальная, как, например, предсказать точную траекторию, по которой именно с этого стола скатится именно этот металлический шарик, положенный именно в это конкретное место.
Да, законы гравитации нам известны, однако предсказать в точности, как именно будет происходить его движение, практически невозможно.
То же самое с нейронной сетью. Принципы её работы ясны, однако понять, как она сработает в каждом конкретном случае не представляется возможным.
Строение биологического нейрона
Для того, чтобы понять, как устроен искусственный нейрон, начнем с того, что вспомним, как функционирует обычная нервная клетка.
Именно её строение и функционирование Уоррен Мак-Каллок (американский нейропсихолог, нейрофизиолог, теоретик искусственных нейронных сетей и один из основателей кибернетики) и Уолтер Питтс (американский нейролингвист, логик и математик) взяли как основу для модели искусственного нейрона еще в середине прошлого века.
Итак, как выглядит нейрон?
У нейрона есть тело, которое накапливает и некоторым образом преобразует сигнал, приходящий к нему через дендриты – короткие отростки, функция которых заключается в приеме сигналов от других нервных клеток.
Накопив сигнал, нейрон передает его по цепочке дальше, другим нейронам, но уже по длинному отростку – аксону, который, в свою очередь, связан с дендритами других нейронов, и так далее.
Конечно, это крайне примитивная модель, т.к. каждый нейрон в нашем мозге может быть связан с тысячами других нейронов.
Следующий момент, на который нам нужно обратить внимание — это то, каким образом сигнал передается от аксона к дендритам следующего нейрона.
Дендриты с аксонами связаны не напрямую, а через так называемые синапсы, и когда сигнал доходит до конца аксона, в синапсе происходит выброс нейромедиатора, определяющего, как именно сигнал будет передан дальше.
Нейромедиаторы — биологически активные химические вещества, посредством которых осуществляется передача электрохимического импульса от нервной клетки через синаптическое пространство между нейронами, а также, например, от нейронов к мышечной ткани или железистым клеткам.
Иными словами, нейромедиатор является неким мостиком между аксоном и множеством дендритов принимающей сигнал клетки.
Если нейромедиатора выбросится много, то сигнал передастся полностью или даже будет усилен. Если же его выбросится мало, то сигнал будет ослаблен, либо вовсе погашен и не передастся другим нейронам.
Таким образом, процесс формирования устойчивых нейронных дорожек (уникальных цепочек связей между нейронами), являющихся биологической основой процесса обучения, зависит от того, сколько будет выброшено нейромедиатора.
Все наше обучение с самого детства построено именно на этом и образование совсем новых связей происходит достаточно редко.
В основном, все наше обучение и освоение новых навыков связано с настройкой того, сколько выбрасывается нейромедиатора в местах контакта аксонов и дендритов.
Например, у нас в нервной системе может быть установлена связь между понятиями «яблоко» и «груша». Т.е. как только мы видим яблоко, слышим слово «яблоко» или просто думаем о нем, в нашем мозгу по механизму ассоциации возникает образ груши, т.к. между этими понятиями у нас установлена связь — сформирована нейронная дорожка.
При этом между понятиями «яблоко» и подушка такой связи, скорее всего нет, поэтому образ яблока не вызывает у нас ассоциаций с подушкой — соответствующая нейронная связь очень слаба или отсутствует.
Интересный момент, о котором также стоит упомянуть, заключается в том, что у насекомых аксон и дендрит связаны напрямую, без выделения нейромедиатора, поэтому у них не происходит обучения в том виде, как оно происходит у животных и человека.
В течение жизни они не обучаются. Они рождаются уже умея всё, что они будут уметь в течение жизни.
Например, каждая конкретная муха не способна научиться облетать стекла — она всегда будет в них биться, что мы с вами постоянно и наблюдаем.
Все нейронные связи у них уже существуют, а сила передачи каждого сигнала уже отрегулирована определенным образом для всех имеющихся связей.
С одной стороны, такая негибкость в плане обучения имеет свои минусы.
С другой стороны, у насекомых, как правило, очень высокая скорость реакции именно за счет другой организации нервных связей, за счет отсутствия веществ-медиаторов, которые, будучи своеобразными посредниками, естественным образом замедляют процесс передачи сигналов.
При всем при этом обучение у насекомых все же может происходить, но только эволюционно — от поколения к поколению.
Допустим, если способность облетать стекла станет важных фактором, способствующим выживанию вида как такового, то за некоторое количество поколений все мухи в мире научатся их облетать.
Пока этого не могло произойти, т.к. сами стекла существую относительно недавно, а для эволюционного обучения могут потребоваться десятки тысяч лет.
Если же это не играет важной роли для сохранения вида, то данная модель поведения так и не будет ими приобретена.
Обычно при работе математическими нейронами используются следующие обозначения:
X – входные данные
W – веса
H – тело нейрона
Y – выход нейронной сети
Входные данные — это сигналы, поступающие к нейрону.
Веса – это эквиваленты синаптической связи и выброса нейромедиатора, представленные в виде чисел, в том числе отрицательных.
Вес представлен действительным числом, на которое будет умножено значение входящего в нейрон сигнала.
Иными словами, вес показывает, насколько сильно между собой связаны те или иные нейроны — это коэффициент связи между ними.
В теле нейрона накапливается взвешенная сумма от перемножения значений входящих сигналов и весов.
Если говорить кратко, то процесс обучения нейронной сети — это процесс изменения весов, т.е. коэффициентов связи между имеющимися в ней нейронами.
В процессе обучения веса меняются, и, если вес положительный, то идет усиление сигнала в нейроне, к которому он приходит.
Если вес нулевой, то влияние одного нейрона на другой отсутствует. Если же вес отрицательный, то идет погашение сигнала в принимающего нейроне.
И, наконец, выход нейронной сети — это то, что мы получаем в результате обработки нейроном поданного на него сигнала. Это некая функция от накопившейся в теле нейрона взвешенной суммы.
Эти функции бывают разные, например, если мы имеем дело с функцией Хевисайда, то если в теле нейрона накопилась сумма больше определенного порога, то на выходе будет единица. Если же меньше порога — ноль.
На выходе из нейрона возможны разные значения, однако чаще всего их стараются приводить к диапазону от -1 до 1 или, что бывает еще чаще — от 0 до 1.
Еще один пример — это функция ReLU (на иллюстрации не представлена), которая работает иначе: если значение взвешенной суммы в теле нейрона отрицательно, то идет преобразование в 0, а если положительно, то в X.
Так, если значение на нейроне было -100 то после обработки функцией ReLU оно станет равным 0. Если же это значение равно, например, 15,7, то на выходе будут те же 15,7.
Также для обработки сигнала с тела нейрона применяются сигмоидальные функции (логистическая и гиперболический тангенс) и некоторые другие. Обычно они используются для «сглаживания» значений некоторой величины.
К счастью, для написания кода необязательно понимать различные формулы и графики этих функций, т.к. они уже заложены в библиотеках, используемых для работы с нейронными сетями.
Простейший пример классификации 2 объектов
Теперь, когда мы разобрались с общим принципом работы, давайте посмотрим на простейший пример, иллюстрирующий процесс классификации 2 объектов с помощью нейронной сети.
Представим, что нам нужно отличить входной вектор (1,0) от вектора (0,1). Допустим, черное от белого, или белое от черного.
При этом, в нашем распоряжении есть простейшая сеть из 2 нейронов.
Допустим, что мы зададим верхней связи вес «+1», а нижней – «-1». Теперь, если мы подадим на вход вектор (1,0), то на выходе мы получим 1.
Если же мы подадим на вход вектор (0,1), то на выходе получим уже -1.
Для упрощения примера будем считать, что у нас здесь используется тождественная активационная функция, при которой f(x) равно самому x, т.е. функция никак не преобразует аргумент.
Таким образом, наша нейронная сеть может классифицировать 2 разных объекта. Это совсем-совсем примитивный пример.
Обратите внимание на то, что в данном примере мы сами назначаем веса «+1» и «-1», что не совсем неправильно. В действительности они подбираются автоматически в процессе обучения сети.
Обучение нейронной сети – это подбор весов.
Любая нейронная сеть состоит из 2 составляющих:
- Архитектура
- Веса
Архитектура — это её структура и то, как она устроена изнутри (об этом мы поговорим чуть позже).
И когда мы подаем на вход нейронной сети данные, нам нужно её обучить, т.е. подобрать веса между нейронами так, чтобы она действительно выполняла то, что мы от неё ожидаем, допустим, умела отличать фото кошек от фото собак.
На самом деле, обучением нейронной сети можно назвать также и подбор архитектуры в процессе исследования.
Так, например, если мы работаем с генетическими алгоритмами, то в процессе обучения нейронная сеть может менять не только веса, но еще и свою структуру.
Но это скорее исключение, чем правило, поэтому в общем случае под обучением нейронной сети мы будем понимать именно процесс подбора весов.
1. С учителем
Обучение с учителем — это одна из самых частых задач. В этом случае у нас есть выборка, и мы знаем по ней правильные ответы.
Мы точно заранее знаем, что на этих 10 тыс. фото — собаки, а на этих 10 тыс. фото — кошки.
Или мы точно знаем курс каких-нибудь акций на протяжении длительного времени.
Допустим, 5 дней до этого дня были такие цены, а на следующий день цена стала такой-то. Затем смещаемся на день вперед, и мы снова знаем: 5 дней цены были такие, а на следующий день цена стала такой-то, и так далее.
Или мы знаем характеристики домов и их реальные цены, если мы говорим про прогнозирование цен на недвижимость.
Все это — примеры обучения с учителем.
2. Без учителя
Второй тип обучения — это обучение без учителя (кластеризация).
В этом случае мы подаем на вход сети некие данные и предполагаем, что в них можно выделить несколько классов.
Пример: у нас есть большая база email-адресов людей, с которыми мы взаимодействуем.
Они открывают наши письма, читают их, переходят или не переходят по ссылкам. Они делают это в то или иное время суток и т.д.
Обладая этой информацией (например, благодаря статистике почтового рассыльщика), мы можем передать нейронной сети эти данные с «просьбой» выделить несколько классов читателей, которые по тем или иным критериям будут схожи между собой.
Используя эти данные, мы смогли бы выстраивать более эффективную маркетинговую стратегию взаимодействия с каждой из таких групп людей.
Обучение без учителя используется в тех случаях, когда требуется обнаружить внутренние взаимосвязи, зависимости и закономерности, существующие между объектами, но заранее мы не знаем, в чем именно заключаются эти закономерности.
3. С подкреплением
Третий тип обучения — это обучение с подкреплением. Оно немного похоже на обучение с учителем, когда есть готовая база верных ответов. Отличие заключается в том, что в этом случае у нейронной сети нет всей базы сразу, а дополнительные данные приходят в процессе обучения и дают сети обратную связь о том, достигнута цель или нет.
Простой пример: бот проходит некий лабиринт и на 59 шаге получает информацию о том, что он «упал в яму» или после 100 шагов, выделенных на выход из лабиринта, он узнает, что «не дошел до выхода».
Таким образом нейронная сеть понимает, что последовательность её действий не привела к нужному результату и обучается, корректируя свои действия.
Т.е. заранее сеть не знает, что верно, а что – нет, но получает обратную связь, когда происходит некое событие. По этому принципу строятся практически все игры, а также различные агенты и боты.
Кстати, пример с умирающими смайликами, который мы рассматривали выше — это также обучение с подкреплением.
Кроме того, это автопилоты, подбор стиля под пожелания конкретного человека, исходя из его предыдущих выборов и т.д.
Например, нейронная сеть генерирует 20 картинок и человек выбирает из них 3, которые ему больше всего нравятся. Затем нейронная сеть создает еще 20 картинок и человек снова выбирает то, что ему нравится больше всего и т.д.
Таким образом, сеть выявляет предпочтения человека по стилю изображения, цветовой гамме и другим характеристикам, и может создавать изображения под вкусы каждого конкретного человека.
Одной из ключевых задач при создании нейронных сетей является выбор её архитектуры. Он производится разработчиком, исходя из стоящей перед ним задачи и, в общем случае, нейронная сеть обучается только за счет изменения весов.
Исключение – генетические алгоритмы, когда популяция нейронных сетей в процессе обучения самостоятельно пересобирает саму себя и свою архитектуру в том числе.
Однако в подавляющем большинстве случаев в процессе обучения архитектура нейронной сети самопроизвольно не меняется.
Все изменения в нее вносит сам разработчик через добавление или удаление слоев, увеличение или уменьшение количества нейронов на каждом слое и т.д.
Давайте посмотрим на основные архитектуры и начнем с классического примера — полносвязной нейронной сети прямого распространения.
Полносвязная нейронная сеть прямого распространения — это сеть, в которой каждый нейрон связан со всеми остальными нейронами, находящимися в соседних слоях, и в которой все связи направлены строго от входных нейронов к выходным.
Слева на рисунке мы видим входной слой, на который приходит сигнал. Правее находятся два скрытых слоя, и самый правый слой из двух нейронов – выходной слой.
Эта сеть может решить задачу классификации, если нам нужно выделить два любых класса – допустим, кошек и собак.
Например, верхний из этих двух нейронов отвечает за класс «собаки» и может иметь значения 0 или 1. Нижний – за класс «кошки», и имеет возможность принимать те же самые значения – 0 или 1.
Другой вариант — она может, к примеру, прогнозировать 2 параметра — давление и температуру в котле на основании 3 неких входных данных.
Чтобы лучше понять, как это работает, давайте посмотрим, как такая полносвязная сеть может использоваться, допустим, для прогнозирования погоды.
В этом примере мы подаем на вход температурные данные по трем последним дням, а на выходе хотим получить предсказания на два следующих дня.
Между входным и выходным слоями у нас есть 2 скрытых слоя, в которых нейронная сеть будет обобщать данные.
Например, в первом скрытом слое один нейрон будет отвечать за такое простое понятие, как «температура растет».
Другой нейрон будет отвечать за понятие «температура падает».
Третий – за понятие «температура неизменна».
Четвертый – «температура очень высокая».
Пятый – «температура высокая».
Шестой – «температура нормальная».
Седьмой – «температура низкая».
Восьмой – «совсем низкая».
Девятый – «температура скакнула вниз и вернулась».
И так далее.
Мы, разумеется, называет все это словами, понятными нам, в то время как сеть сама в процессе обучения сделает так, что каждый из нейронов будет за что-то отвечать, чтобы эффективно обобщать данные.
И каждый из нейронов первого скрытого слоя (в т.ч. обведенный на картинке) будет отвечать за выделение определенного признака в поступивших данных.
Например, если он отвечает за понятие «температура растет», то нейрон с температурой +17 со входного слоя будет входить в него с положительным весом.
Нейрон с температурой +15 – с нулевым весом, а нейрон с температурой +12 – с отрицательным весом.
Если этот так, т.е. температура реально росла и подавались такие данные, обведенный на рисунке нейрон будет активироваться. Если же температура падала, он останется неактивированным.
Нейроны второго скрытого слоя (например, обведенный на картинке ниже), будут отвечать за более высокоуровневые признаки, например, за то, что это осень. Или за то, что это любое другое время года.
На активацию данного нейрона будет положительно влиять падение температуры, свойственное осени.
Если наблюдается постепенный рост температуры, то, скорее всего, это статистически будет влиять в минус, т.е. не будет активировать данный нейрон.
Понятно, что бывают ситуации, когда осенью температура растет, но, скорее всего, статистически рост температуры в течение 3 дней будет отрицательно влиять на этот нейрон.
То, что температура слишком высокая, точно будет отрицательно влиять на нейрон, отвечающий за осень, т.к. не бывает очень высоких температур в это время года, и так далее.
И, наконец, нейроны 2 скрытого слоя соединяются с двумя выходными нейронами, которые выдают предсказание по температуре.
При этом обратите внимание, что сами мы не задаем, за что будет отвечать каждый из нейронов (это было бы примером классического машинного обучения). Нейронная сеть сама сделает нейроны «ответственными» за те или иные понятия.
Более того, в рамках нейронной сети привычные нам понятия, такие как «весна», «осень», «температура падает» и т.д. не существуют. Мы просто обозначили их так для своего удобства, чтобы нам было понятнее, что примерно происходит внутри во время обучения.
В действительности же нейронная сеть сама в процессе обучения выделит именно те свойства, которые нужны для решения данной конкретной задачи.
Важно понимать, что нейронная сеть именно с такой архитектурой не будет правильно прогнозировать температуру – это упрощенный пример, показывающий, как внутри сети может происходить выделение различных признаков (так называемый процесс feature extraction).
Процесс выделения признаков хорошо иллюстрирует работа свёрточных нейронных сетей.
Допустим, на вход сети мы подаем фото кошек и собак, и сеть начинает их анализировать.
На первых слоях сеть определяет наиболее общие признаки: линия, круг, темное пятно, угол, близкий к прямому, яркое пятно на фоне темного и т.д.
На следующем слое сеть сможет извлечь уже иные признаки: ухо, лапа, голова, нос, хвост и т.д.
Еще дальше, на следующем слое будет идет анализ уже такого плана:
— здесь хвост загнут вверх, это коррелирует с лайкой, значит собака;
— а тут короткие лапы, а у кошек нет коротких лап, поэтому, скорее всего, собака, такса;
— а вот здесь треугольное ухо с белым кончиком, похоже на такую-то породу кошки, и т.д.
Примерно таким образом происходит процесс feature extraction.
Безусловно, чем больше нейронов в сети, тем более детальные, специфические свойства она может выделить.
Если же нейронов немного, она сможет работать только «крупными мазками», т.к. у нее не хватит мощности проанализировать все возможные комбинации признаков.
Таким образом, общий принцип выделения свойств — это все большее обобщение при переходе с одного слоя на другой, от низкого уровня ко все более высокому.
Итак, давайте снова вспомним, что из себя представляет нейронная сеть.
Во-первых, это архитектура.
Она задается разработчиком, т.е. мы сами определяем, сколько у сети слоев, как они связаны, сколько нейронов на каждом слое, есть ли у этой сети память (об этом чуть позже), какие у нее активационные функции и т.д.
Архитектуру сети можно представить, например, как обычный JSON, YAML или XML-файл (текстовые файлы, в которых можно полностью отразить её структуру).
Во-вторых, это обученные веса.
Веса нейронной сети хранятся в совокупности матриц в виде действительных чисел, количество которых (чисел) равно общему количеству связей между нейронами в самой сети.
Количество самих матриц зависит от количества слоев и того, как нейроны разных слоев связаны друг с другом.
Веса задаются в начале обучения (обычно случайным образом) и подбираются в процессе обучения.
Таким образом, сеть можно легко сохранить через сохранение её архитектуры и весов, а в дальнейшем снова использовать её, обратившись к файлам с архитектурой и весами.
Обучение полносвязной сети методом обратного распространения ошибки (градиентного спуска)
Как же обучается полносвязная нейронная сеть прямого распространения? Давайте рассмотрим этот вопрос на примере метода обратного распространения ошибки, являющегося модификацией метода классического градиентного спуска.
Это метод обновления весов нейронной сети, при котором распространение сигналов ошибки происходит от выходов сети к её входам, в направлении, обратном прямому распространению сигналов в обычном режиме работы.
Сразу отметим, что это не самый продвинутый алгоритм, и при работе с библиотекой Keras мы будем использовать более эффективные решения.
Также важно понимать, что в других типах сетей обучение происходит по другим алгоритмам, однако сейчас нам важно понять общий принцип.
Вернемся к примеру про прогнозирование температуры и допустим, что в качестве выходного сигнала у нас есть 2 вещественных числа т.е. значения температуры в градусах по 2 дням.
Таким образом, мы знаем правильные ответы, т.е. какая температура была на самом деле, и знаем те ответы, которые дала нам нейронная сеть.
В качестве способа измерения величины ошибки при обучении сети мы будем использовать так называемую функцию среднеквадратичной ошибки.
Например, в первый день сеть предсказала нам 10 градусов, а реально было 12. Про второй день она сказала, что будет 11 градусов, и реально также было 11.
Среднеквадратичная ошибка в этом случае составит сумму квадратов разностей предсказанной и реальной температур: ((10-12)² + (11-11)²)/2 = 2.
В процессе обучения нейронной сети алгоритмы (в т.ч. Back Propagation – алгоритм обратного распространения ошибки) ориентированы на то, чтобы менять веса так, чтобы уменьшать эту среднеквадратичную ошибку.
Если она будет равна нулю, это значит, что нейронная сеть гарантированно точно распознает, например, все образы в обучающей выборке.
Понятно, что на практике в ноль она никогда не сходится, но близко к нулю — вполне возможно, и точность в этом случае будет очень высокая.
Метод градиентного спуска
Представим теперь, что у нас есть некоторая поверхность ошибки. Допустим, у нас есть нейронная сеть, в которой 2 веса, и мы можем эти веса менять.
Понятно, что разным значениям этих весов будет соответствовать разная ошибка нейронной сети.
Таким образом, взяв эти значения, мы можем создать двухмерную поверхность, на которой видны соотношения значений весов W₁ и W₂ и значений ошибок X₀-X₄ (см. иллюстрацию).
Теперь представим, что у нас в нейронной сети не два, а тысяча весов, и разные комбинации этих тысяч весов соответствуют разным ошибкам при одной и той же обучающей выборке. В этом случае у нас получается уже тысячемерная поверхность.
Мы можем менять тысячу действительных чисел и каждый раз получать новое значение ошибки.
И эту тысячемерную поверхность можно сравнить с поверхностью измятого одеяла со своими локальными максимумами и минимумами (соответственно максимальными и минимальными значениями функции на заданном множестве). Это и будет называться поверхностью ошибки.
Так вот, обучение нейронной сети – это продвижение по этой тысячемерной поверхности разных весов и получение разных ошибок. И где-то на этой поверхности есть глобальный минимум – такая комбинация этой тысячи весов, при которой ошибка минимальна.
Локальных же минимумов может быть много, и в них значение ошибки достаточно низкое.
Поэтому обучение нейронной сети можно представить как это осмысленное перемещение по этой тысячемерной поверхности с целью добиться минимальной ошибки в некоторой точке.
При этом глобальный минимум, как правило, никто не ищет – он один, а пространство для поиска огромное. Именно поэтому нашей целью обычно являются как раз локальные минимумы. При всем при этом стоит отметить, что заранее мы не можем знать, какой именно минимум мы нашли – один из локальных, либо глобальный.
Но как именно происходит перемещение по поверхности ошибки? Давайте разберемся.
Важный момент заключается в том, что мы можем посчитать производную ошибки по весам. Иными словами, мы можем математически просчитать, как изменится ошибка, если мы чуть-чуть изменим в какую-то сторону веса.
Таким образом, мы можем вычислить, куда нам нужно сместить веса, чтобы ошибка у нас гарантированно уменьшилась при следующем шаге обучения.
Именно таким образом мы пошагово ищем необходимое направление спуска по поверхности ошибки.
Если мы шагнем слишком широко, то можем перескочить локальный минимум и подняться выше по поверхности ошибки, что для нас нежелательно.
Если же шаг будет небольшим, то мы шагнем вниз, ближе к локальному минимуму, затем снова пересчитаем направление движения и снова сделаем небольшой шаг.
Да, продвинутые алгоритмы могут, например, «перелезть через горку» и найти более глубокий локальный минимум и т.д., но в целом алгоритм обучения построен именно так.
Мы работаем с многомерной поверхностью ошибки в рамках наших весов и ищем, куда нам двигаться, чтобы прийти в локальный минимум, по возможности, максимально глубокий.
Такая архитектура позволяет поэтапно выделять разные признаки на разных слоях. Первые слои при этом выделяют самые простые признаки, например:
— справа – белое, слева – черное
— по центру – яркое, по краям – тусклое
— линия под 45 градусов
— и т.д.
На иллюстрации ниже изображена одномерная свёрточная сеть, в которой не все нейроны связаны со всеми, а каждый нейрон последующего слоя «смотрит» на два нейрона предыдущего слоя (за исключением входного слоя слева).
При этом вы можете обратить внимание, что в правой части свёрточная сеть переходит в полносвязную, т.е. идет объединение двух архитектур. Для чего это нужно вы узнаете буквально несколькими предложениями ниже.
Выделение признаков происходит постепенно, слой за слоем, например: линия – овал – лапа, что отражено на трех иллюстрациях ниже.
В генеративной сети у нас две нейронных сети: одна генерирует образы, а вторая пытается отличить эти образы от некоторых эталонных.
Например, одна сеть-детектор смотрит на фентези-картинки, а другая пытается создавать новые картинки и говорит: «А попробуй отличить фентези-картинки от моих».
Сначала она генерирует картинки, которые сеть-детектор легко отличает, потому что они не очень похожи на фентези-картинки.
Но потом та сеть, которая создает картинки, обучается все больше, и сеть-детектор уже едва может отличить фентези-картинки, которые мы подали ей на вход сами от тех, что сгенерировала вторая сеть.
И сейчас, когда мы рассмотрели основные архитектуры, давайте вспомним типы задач, которые решаются нейронными сетями и сопоставим их с архитектурами, которые для подходят для их решения.
1. Распознавание образов производится средствами полносвязных, свёрточных, рекуррентных сетей.
2. Языковые задачи чаще всего решаются с помощью рекуррентных сетей с памятью, и иногда — с помощью одномерных свёрточных.
3. Обработка аудио и голоса — это полносвязные, одномерные свёрточные, иногда рекуррентные сети.
4. Задачи регрессии и прогнозирования временных рядов решаются с помощью полносвязных и рекуррентных сетей.
5. Наконец, машинное творчество — это генеративные свёрточные и генеративные полносвязные сети.
Таким образом, мы видим, что стандартные архитектуры пересекаются и используются в задачах разного типа.
Мы будем проходить все эти архитектуры: сначала полносвязные, потом свёрточные, рекуррентные и генеративные. После чего уже будем изучать сложные комбинации архитектур.
На этом шаге мы берем какую-нибудь архитектуру исходя из имеющейся задачи и получаем первичные результаты на обучающей и тестовой выборках.
Допустим, у нас задача распознавания кошек и собак. Значит берем сверточную сеть.
Попробуем простую сеть на 5 слоев по 20 нейронов в каждой, а в конце — 2 слоя полносвязной, и обучим.
В результате мы получаем какие-то базовые результаты, некую первичную точность. И уже после этого начинаем думать, как можно её повысить, например:
— создаем различные варианты архитектур (допустим, несколько десятков вариантов) и собираем статистику по ошибке и точности для каждого из вариантов
— пробуем разные активационные функции
— повышаем качество данных (избавление от зашумленных данных)
— и так далее…
В итоге, путем экспериментов и проверки своих предположений мы доводим качество работы сети до желаемого уровня.
И, наконец, наша production-часть обращается к сервису и производит распознавание.
После вывода в production, разумеется, можно продолжить исследования и постепенно улучшать работу сети и подменять её по мере доработки — либо архитектуру, либо веса, либо и то, и другое.
Скажем, вы обучили сеть на относительно небольшом объеме данных, а через некоторое время вам пришло данных в 10 раз больше. Безусловно, стоит переобучить сеть на большем объеме данных и, вполне возможно, повысить качество её работы.
Для этого мы постоянно сравниваем результаты работы нашей сети по точности распознавания на тестовой выборке.
При этом важно постоянно помнить о том, что в области нейронных сетей даже близко нет детерминированных ответов — это всегда исследование и изобретение.
Итак, мы дошли с вами до нашего главного инструмента в создании нейронных сетей — библиотек.
Что такое библиотека? Это набор функций, которые позволяют нам собрать и обучить нейронную сеть без необходимости глубоко понимать математическую базу, лежащую в основе этих процессов.
Совсем недавно библиотек еще не было, но сейчас уже есть достаточно хороший выбор, существенно облегчающий жизнь разработчика и экономящий его время.
Мы не будем останавливаться на каждой библиотеке отдельно — это, скорее, мини-обзор существующих решений, чтобы вы могли в целом в них ориентироваться.
Так, на настоящее время наиболее известными являются следующие библиотеки:
- TensorFlow (Google)
- CNTK (Microsoft)
- Theano (Монреальский институт алгоритмов обучения (MILA))
- Pytorch (Facebook)
- и так далее
- Сама видеокарта (GPU)
- CUDA (программно-аппаратная архитектура параллельных вычислений, которая позволяет существенно увеличить вычислительную производительность благодаря использованию графических процессоров фирмы Nvidia)
- cuDNN (библиотека, содержащая оптимизированные для GPU реализации сверточных и рекуррентных сетей, различных функций активации, алгоритма обратного распространения ошибки и т.п., что позволяет обучать нейронные сети на GPU в несколько раз быстрее, чем просто CUDA)
- TensorFlow, CNTK, Theano (надстройки над cuDNN)
Keras же, в свою очередь, использует в качестве бэкенда TensorFlow и некоторые другие библиотеками, позволяя нам пользоваться уже готовым кодом, написанными именно для создания нейронных сетей.
Еще один важный момент, который нужно отметить — это вопрос о том, как со всем этим работает Python. Многие говорят, что он работает медленно.
Да, это так, но Keras генерирует не код Python, Keras генерирует код С++, и именно он уже используется непосредственно для работы нейронной сети, поэтому все работает быстро.
Наш учебный процесс будет построен именно вокруг Google Colaboratory (демонстрации кода, домашние задания и т.д.), и в целом это очень удобно, однако, если вы хотите, то можете работать и в своей, привычной вам среде.
Запускать Google Colaboratory рекомендуется под Google Chrome, и при первой попытке открыть ноутбук (так называется любой документ в Google Colaboratory) вам будет предложено установить в браузер приложение Colaboratory, чтобы в будущем оно автоматически «подхватывало» файлы такого типа.
Теперь, после установки Colaboratory мы можем перейти уже непосредственно к практической части.
В первой строке мы импортируем предустановленный датасет MNIST с изображениями рукописных цифр, которые мы будем распознавать.
Затем мы подключаем библиотеки Sequential и Dense т.к. работаем с полносвязной сетью прямого распространения.
Наконец, мы импортируем дополнительные библиотеки, позволяющие нам работать с данными (NumPy, Matplotlib и др.).
Сразу обратите внимание на модульную структуру ноутбука, благодаря которой вы можете в любом порядке чередовать блоки кода и текстовые поля.
Для того, чтобы добавить новую ячейку кода, текстовое поле или поменять порядок их следования воспользуйтесь, кнопками CODE, TEXT и CELL прямо под основным меню.
Ну и, наконец, для запуска фрагмента кода нам нужно просто нажать на значок play слева вверху от кода. Он появляется при выделении блока с кодом, либо при наведении мышки на пустые квадратные скобки, если блок не выделен.
Следующим шагом с помощью функции load_data мы подгружаем данные, необходимые для обучения сети, где x_train_org и y_train_org – данные обучающей выборки, а x_test_org и y_test_org – данные тестовой выборки.
Их названий выборок понятно, что обучающую выборку мы используем для того, чтобы обучить сеть, в то время как тестовая используется для того, чтобы проверить, насколько качественно произошло это обучение.
Смысл тестовой выборки в том, чтобы проверить, насколько точно отработает наша сеть с данными, с которыми она не сталкивалась ранее. Именно это является самой важной метрикой оценки сети.
В x_train_org находятся сами изображения цифр, на которых обучается сеть, а y_train_org – правильные ответы, какая именно цифра изображена на том или ином изображении.
Сразу важно отметить, что формат представления правильных ответов на выходе из сети — one hot encoding. Это одномерный массив (вектор), хранящий 10 цифр – 0 или 1. При этом положение единицы в этом векторе и говорит нам о верном ответе.
Например, если цифра на картинке изображен 0, то вектор будет выглядеть как в первой строке на картинке ниже.
Если на изображении цифра 2, то единица в векторе будет стоять в 3 позиции (как в строке 2).
Если же на изображении цифра 9, то единица в векторе будет стоять в последней, десятой позиции.
Это объясняется тем, что нумерация в массивах по умолчанию начинается с нуля.
Изначально на вход сети в обучающей выборке подается 60 тыс. изображений размером 28 на 28 пикселей. В тестовой выборке таких изображений 10 тыс. штук.
Наша задача — привести их к одномерному массиву (вектору), размерность которого будет не 28 на 28, а 1 на 784, что мы и делаем в коде выше.
Следующий наш шаг – это так называемая нормализация данных, их «выравнивание» с целью привести их значения к диапазону от 0 до 1.
Дело в том, что до нормализации изображение каждой рукописной цифры (все они представлены в градациях серого) представлено числами от 0 до 255, где 0 представляет собой чёрный цвет, а 255 — белый).
Однако для эффективной работы сети нам необходимо привести их к другому диапазону, что мы и делаем, разделив каждое значение на 255.
В данном случае количество нейронов (800) уже подобрано эмпирически. При таком количестве нейронная сеть обучается лучше всего (безусловно, на старте своих исследований мы можем пробовать самые разные варианты, постепенно приходя к оптимальному).
В качестве выходного слоя у нас будет полносвязный слой из 10 нейронов – по количеству рукописных цифр, которые мы распознаём.
И наконец, функция softmax будет преобразовывать вектор из 10 значений в другой вектор, в котором каждая координата представлена вещественным числом в интервале [0,1] и сумма этих координат равна 1.
Эта функция применяется в машинном обучении для задач классификации, когда количество классов больше двух, при этом полученные координаты трактуются как вероятности того, что объект принадлежит к определенному классу.
После этого мы компилируем сеть и выходим на экран ее архитектуру.
Сначала мы записываем сеть в файл, затем можем проверить, что он сохранился, после чего сохраняем файл на компьютер с помощью метода download.
Ну что ж, теперь настало время проверить, как наша сеть распознает рукописные цифры, которые она еще не видела. Для этого нам понадобится тестовая выборка, которая, наряду с обучающей, также входит в датасет MNIST.
Для начала выберем произвольную цифру из набора тестовых данных и выведем её на экран:
Теперь, когда мы получили некие первичные результаты, мы можем продолжить процесс исследования, меняя различные параметры нашей нейронной сети.
Например, мы можем поменять количество нейронов во входном слое и уменьшить их количество с 800, например, до 10.
Для того, чтобы запустить сеть обучаться повторно после внесенных изменений с нуля, нам необходимо заново пересобрать модель, запустить код с измененными параметрами сети и снова её скомпилировать (в противном случае сеть будет дообучаться, что требуется далеко не всегда).
Для этого мы просто заново нажимаем на play последовательно во всех трех блоках кода (см. ниже).
В результате этих действий вы увидите, что исходные данные изменились, и сеть теперь имеет 2 слоя по 10 нейронов, вместо 800 и 10, которые были ранее.
Как мы видим, даже при одном нейроне в скрытом слое сеть достигла точности почти в 38%. Понятно, что с таким результатом она едва ли найдет практическое применение, однако мы делаем это просто для понимания того, как могут разниться результаты при изменении архитектуры.
Итак, завершая разбор кода, следует сделать важную оговорку: то, что мы смотрим точность на обучающей выборке – это просто пример для понимания того, как это работает.
В действительности, качество работы сети нужно замерять исключительно на тестовой выборке с данными, с которыми нейронная сеть еще не сталкивалась ранее.
И теперь, когда мы немного поэкспериментировали, давайте подведем небольшой итог и посмотрим, как подойти к проведению экспериментов с нейронными сетями системно, чтобы получать действительно статистически значимые результаты.
Итак, мы берем одну модель сети, и в цикле формируем из имеющихся данных 100 разных обучающих и тестовых выборок в пропорции 80% — обучающая выборка, и 20% — тестовая.
Далее на всех этих данных мы проводим 100 обучений нейронной сети со случайной точки (каждый раз сеть стартует со случайными весами) и получаем некую ошибку на тестовой выборке — среднюю за эти 100 обучений на данной конкретной архитектуре (но с разными комбинациями обучающей и тестовой выборок).
Потом берется другая архитектура и делается то же самое. Таким образом мы можем проверить, например несколько десятков вариантов архитектур по 100 обучений на каждой, в результате чего получим статистически значимые результаты своих экспериментов и сможем выбрать самую точную сеть.
Ну что ж, на этом мы завершаем данный обзор и надеемся, что он помог вам лучше понять как работают нейронные сети, и как можно быстро написать свою первую нейронную сеть на Python.
Введение
Машинное обучение становится все более популярным, и многие наверняка слышали о глубоком обучении. Интересно узнать, как использовать такое обучение в языке MQL. Существуют простые реализации искусственных нейронов с функциями активации, но я пока не встречал ничего, что бы реализовало настоящую глубокую нейросеть. Статья познакомит вас с глубокой нейронной сетью, написанной на MQL, и с различными функциями активации этой сети, такими как функция гиперболического тангенса для скрытых слоев и Softmax для выходного слоя. Мы будем изучать нейросеть постепенно, двигаясь от первого шага до последнего, и вместе создадим глубокую нейронную сеть.
1. Создание искусственного нейрона
Начнем с базовой единицы нейросети — нейрона. В статье мы будем рассматривать различные части того типа нейрона, которые будем использовать в нашей глубокой нейросети. Строго говоря, отличие нашего типа нейрона от других заключается в одной части — функции активации.
1.1. Части нейрона
Искусственный нейрон, смоделированный на основе биологического прототипа — нейрона человеческого мозга — просто выполняет математические вычисления. Как и наши нейроны, он срабатывает, когда сталкивается с достаточным количеством раздражителей. Нейрон комбинирует входные данные с набором коэффициентов или весов, которые либо усиливают, либо ослабляют эти входные данные. Таким образом придается значимость входным данным для задачи, которую пытается обучить алгоритм. На изображении ниже показано, как работают различные части нейрона:
1.1.1. Входные данные
Данные, подаваемые на вход нейрона, являются либо внешним триггером из окружающей среды, либо исходят от выходов других искусственных нейронов. Входные данные служат «пищей» для нейрона и проходят через него. Из них генерируются выходные данные, которые можно интерпретировать в соответствие с обучением, которое получил нейрон. Это могут быть дискретные значения или действительные числа.
1.1.2. Веса
Веса — это коэффициенты, которые увеличивают или уменьшают значения определенных данных. То есть они придают большее или меньшее значение входным данным, поступающим внутрь нейрона, и, следовательно, влияют на выходные данные. Цель алгоритмов обучения нейросети — определить «наилучший» возможный набор значений весов для решения задачи.
1.1.3. Net Input Function
В этой части нейрона входные данные и веса сводятся в одно значение. Здесь вычисляется сумма входных данных, помноженных на соответствующие веса. Далее полученный результат передается в функцию активации, которая затем измеряет и выдает влияние входных нейронов на выходные данные нейронной сети.
1.1.4. Функция активации нейрона
Функция активации дает выходные данные нейросети. Есть разные типы функций активации (Sigmoid, Tan-h, Softmax, ReLU и другие). Такая функция решает, нужно ли активировать нейрон. В этой статье мы будем работать с двумя типами функции активации: Tan-h и Softmax.
1.1.5. Выход
Последняя часть нейрона — это выход. Такой выходной сигнал можно передать на вход другому нейрону или во внешнюю среду. Это может быть дискретное или действительное значение, в зависимости от используемой функции активации.
2. Построение нейронной сети
Нейронная сеть — это парадигма обработки информации, схожая с принципами обработки информации биологическими нервными системами, такими как мозг. Она состоит из слоев искусственных нейронов, при этом каждый слой соединен со следующим. Следовательно, предыдущий слой служит входом для следующего слоя и так далее до выходного слоя. Целью нейронной сети может быть кластеризация посредством обучения без учителя, классификация через обучение с учителем или регрессию. В этой статье мы будем работать над возможностью классификации по трем состояниям: BUY (покупка), SELL (продажа) или HOLD (ожидание). Ниже показана нейросеть с одним скрытым слоем:
3. Масштабирование нейронной сети в глубокую нейросеть
Глубокую нейросеть от более распространенных сетей с одним скрытым слоем отличает количество слоев, составляющих ее глубину. Если в сети более трех слоев (включая входной и выходной), такой случай рассматривается как «глубокое обучение». Таким образом, «глубокий» — это четко определенный технический термин, означающий наличие более чем одного скрытого слоя. Чем глубже вы продвигаетесь в нейронной сети, тем более сложные характеристики могут распознать ваши нейроны, поскольку они агрегируют и рекомбинируют характеристики из предыдущего слоя. Благодаря этому сети глубокого обучения могут обрабатывать очень большие многомерные наборы данных с миллиардами параметров, которые проходят через нелинейные функции. На изображении ниже показана глубокая нейронная сеть с тремя скрытыми слоями:
3.1. Класс глубокой нейросети
Рассмотрим класс, который будем использовать для создания нашей нейронной сети. Класс глубокой нейронной сети — DeepNeuralNetwork. Основной метод создает полностью связанную нейронную сеть прямого распространения 3-4-5-3. Позже, при обучении глубокой нейросети в этой статье, я покажу несколько примеров данных, подаваемых на вход в сеть. Сейчас же остановимся только на создании сети. В классе прописана сеть с двумя скрытыми слоями. Нейронные сети с тремя и более слоями встречаются очень редко. Однако если вы хотите создать сеть с большим количеством слоев, это легко можно сделать с помощью структуры, представленной в этой статье. Веса от входа в слой A хранятся в матрице iaWeights, веса от слоя A к слою B хранятся в матрице abWeights, а веса от слоя B к выходному хранятся в матрице boWeights. Поскольку многомерный массив может быть статическим или динамическим только для первого измерения, тогда как все остальные измерения статические, размер матрицы нужно указать в объявлении матрицы через директиву «#define». Ниже я удалил все другие используемые объявления, чтобы сэкономить место. Полную версию можно посмотреть в приложенных к статье файлах.
Структура программы:
#define SIZEI 4 #define SIZEA 5 #define SIZEB 3 class DeepNeuralNetwork { private: int numInput; int numHiddenA; int numHiddenB; int numOutput; double inputs[]; double iaWeights[][SIZEI]; double abWeights[][SIZEA]; double boWeights[][SIZEB]; double aBiases[]; double bBiases[]; double oBiases[]; double aOutputs[]; double bOutputs[]; double outputs[]; public: DeepNeuralNetwork(int _numInput,int _numHiddenA,int _numHiddenB,int _numOutput) {...} void SetWeights(double &weights[]) {...} void ComputeOutputs(double &xValues[],double &yValues[]) {...} double HyperTanFunction(double x) {...} void Softmax(double &oSums[],double &_softOut[]) {...} };
Для каждого из двух скрытых слоев и для выходного слоя есть массив связанных значений смещения, названных aBiases, bBiases и oBiases соответственно. Локальные выходы для скрытых слоев хранятся в массивах класса aOutputs и bOutputs.
3.2. Вычисление выходов глубокой нейронной сети
В начале метода ComputeOutputs создаются временные массивы для хранения предварительных значений расчетов (до активации). Затем метод вычисляет предварительную сумму входные данные для узлов слоя A, умноженных на веса, добавляет значения смещения, а потом применяет функцию активации. После этого вычисляются локальные выходы уровня B: только что вычисленные выходы из уровня A используются в качестве локальных входов в B. И, наконец, вычисляются окончательные выходные данные.
void ComputeOutputs(double &xValues[],double &yValues[]) { double aSums[]; double bSums[]; double oSums[]; ArrayResize(aSums,numHiddenA); ArrayFill(aSums,0,numHiddenA,0); ArrayResize(bSums,numHiddenB); ArrayFill(bSums,0,numHiddenB,0); ArrayResize(oSums,numOutput); ArrayFill(oSums,0,numOutput,0); int size=ArraySize(xValues); for(int i=0; i<size;++i) this.inputs[i]=xValues[i]; for(int j=0; j<numHiddenA;++j) for(int i=0; i<numInput;++i) aSums[j]+=this.inputs[i]*this.iaWeights[i][j]; for(int i=0; i<numHiddenA;++i) aSums[i]+=this.aBiases[i]; for(int i=0; i<numHiddenA;++i) this.aOutputs[i]=HyperTanFunction(aSums[i]); for(int j=0; j<numHiddenB;++j) for(int i=0; i<numHiddenA;++i) bSums[j]+=aOutputs[i]*this.abWeights[i][j]; for(int i=0; i<numHiddenB;++i) bSums[i]+=this.bBiases[i]; for(int i=0; i<numHiddenB;++i) this.bOutputs[i]=HyperTanFunction(bSums[i]); for(int j=0; j<numOutput;++j) for(int i=0; i<numHiddenB;++i) oSums[j]+=bOutputs[i]*boWeights[i][j]; for(int i=0; i<numOutput;++i) oSums[i]+=oBiases[i]; double softOut[]; Softmax(oSums,softOut); ArrayCopy(outputs,softOut); ArrayCopy(yValues,this.outputs); }
Что происходит «за сценой»: нейронная сеть использует функцию активации гиперболического тангенса (Tan-h) при вычислении выходных сигналов двух скрытых слоев и функцию активации Softmax при вычислении окончательных выходных значений.
- Функция Tan-h (гиперболический тангенс), как и логистическая сигмоида, является сигмоидальной, но в отличие от нее выводит значения в диапазоне (-1, 1). Таким образом, сильно отрицательные входы в Tan-h приведут к отрицательным выходным сигналам. Нулевые входы приводят к выходам около нуля. Я покажу математическую формулу этой функции, а также ее реализацию в MQL.
double HyperTanFunction(double x) { if(x<-20.0) return -1.0; else if(x > 20.0) return 1.0; else return MathTanh(x); }
- Функция Softmax присваивает десятичные вероятности каждому классу при работе с несколькими классами. Эти десятичные вероятности в сумме должны составлять 1.0. Это дополнительное ограничение позволяет обучению сходиться быстрее.
void Softmax(double &oSums[],double &_softOut[]) { int size=ArraySize(oSums); double max= oSums[0]; for(int i = 0; i<size;++i) if(oSums[i]>max) max=oSums[i]; double scale=0.0; for(int i= 0; i<size;++i) scale+= MathExp(oSums[i]-max); ArrayResize(_softOut,size); for(int i=0; i<size;++i) _softOut[i]=MathExp(oSums[i]-max)/scale; }
4. Пример советника, использующего класс DeepNeuralNetwork
Прежде чем приступить к разработке нашего советника, нужно определить, какие данные мы будем подавать на вход в нашу глубокую нейросеть. Нейросеть хорошо классифицирует паттерны, поэтому будем использовать относительные значения японской свечи в качестве входных данных. Это значения размера верхней тени, тела, нижней тени и направления (бычья или медвежья) свечи. Не обязательно так сильно ограничивать количество входных данных, но в нашем случае этого достаточно для тестовой программы.
Наш демонстрационный советник:
Для нейросети со структурой 4-4-5-3 всего нужно (4 * 4) + 4 + (4 * 5) + 5 + (5 * 3) + 3 = 63 веса и значения смещения.
#include <DeepNeuralNetwork.mqh> int numInput=4; int numHiddenA = 4; int numHiddenB = 5; int numOutput=3; DeepNeuralNetwork dnn(numInput,numHiddenA,numHiddenB,numOutput); input double w0=1.0; input double w1=1.0; input double w2=1.0; input double w3=1.0; input double w4=1.0; input double w5=1.0; input double w6=1.0; input double w7=1.0; input double w8=1.0; input double w9=1.0; input double w10=1.0; input double w11=1.0; input double w12=1.0; input double w13=1.0; input double w14=1.0; input double w15=1.0; input double b0=1.0; input double b1=1.0; input double b2=1.0; input double b3=1.0; input double w40=1.0; input double w41=1.0; input double w42=1.0; input double w43=1.0; input double w44=1.0; input double w45=1.0; input double w46=1.0; input double w47=1.0; input double w48=1.0; input double w49=1.0; input double w50=1.0; input double w51=1.0; input double w52=1.0; input double w53=1.0; input double w54=1.0; input double w55=1.0; input double w56=1.0; input double w57=1.0; input double w58=1.0; input double w59=1.0; input double b4=1.0; input double b5=1.0; input double b6=1.0; input double b7=1.0; input double b8=1.0; input double w60=1.0; input double w61=1.0; input double w62=1.0; input double w63=1.0; input double w64=1.0; input double w65=1.0; input double w66=1.0; input double w67=1.0; input double w68=1.0; input double w69=1.0; input double w70=1.0; input double w71=1.0; input double w72=1.0; input double w73=1.0; input double w74=1.0; input double b9=1.0; input double b10=1.0; input double b11=1.0;
В качестве входных данных для нашей сети будем использовать следующую формулу, которая определяет, какой процент от размера свечи составляет каждая из ее частей.
int CandlePatterns(double high,double low,double open,double close,double uod,double &xInputs[]) { double p100=high-low; double highPer=0; double lowPer=0; double bodyPer=0; double trend=0; if(uod>0) { highPer=high-close; lowPer=open-low; bodyPer=close-open; trend=1; } else { highPer=high-open; lowPer=close-low; bodyPer=open-close; trend=0; } if(p100==0)return(-1); xInputs[0]=highPer/p100; xInputs[1]=lowPer/p100; xInputs[2]=bodyPer/p100; xInputs[3]=trend; return(1); }
Теперь можно подавать данные на вход нейросети:
MqlRates rates[]; ArraySetAsSeries(rates,true); int copied=CopyRates(_Symbol,0,1,5,rates); int error=CandlePatterns(rates[0].high,rates[0].low,rates[0].open,rates[0].close,rates[0].close-rates[0].open,_xValues); if(error<0)return; dnn.SetWeights(weight); double yValues[]; dnn.ComputeOutputs(_xValues,yValues);
Далее нейросеть рассчитывает возможность для торговли на основе полученных данных. Напоминаю, что функция Softmax выдает 3 результата на основе суммы 100%. Значения хранятся в массиве yValues, сигналом будет значение выше 60%.
if(yValues[0]>0.6) { if(m_Position.Select(my_symbol)) { if(m_Position.PositionType()==POSITION_TYPE_SELL) m_Trade.PositionClose(my_symbol); if(m_Position.PositionType()==POSITION_TYPE_BUY) return; } m_Trade.Buy(lot_size,my_symbol); } if(yValues[1]>0.6) { if(m_Position.Select(my_symbol)) { if(m_Position.PositionType()==POSITION_TYPE_BUY) m_Trade.PositionClose(my_symbol); if(m_Position.PositionType()==POSITION_TYPE_SELL) return; } m_Trade.Sell(lot_size,my_symbol); } if(yValues[2]>0.6) { m_Trade.PositionClose(my_symbol); }
5. Обучение глубокой нейронной сети через оптимизатор стратегий
Итак, мы реализовали механизм прямого распространения глубокой нейронной сети, но он не выполняет никакого обучения. Обучение будем проводить в тестере стратегий. Далее я покажу, как обучить нейросеть. Обратите внимание, что из-за большого количества входных данных и диапазона параметров обучение можно проводить только в MetaTrader 5. Если нужно, полученные значения оптимизации можно легко скопировать в MetaTrader 4.
Конфигурация тестера стратегий:
Диапазон для обучения весов и смещения может быть от -1 до 1 с шагом 0.1, 0.01 или 0.001. Вы можете попробовать эти значения и посмотреть, какое из них даст лучший результат. Я использовал шаг 0.001:
Запускается тестер в режиме «Только по ценам открытия», потому что советник работает только с закрытыми свечами и нет смысла проверять на каждом тике. Я проводил оптимизацию на таймфрейме H4. Вот такие результаты получились при тестировании на истории за последний год:
Заключение
Описание алгоритма и код, представленные в этой статье, могут послужить хорошей основой для понимания нейронных сетей с двумя скрытыми слоями. А что же с сетями, имеющими три и более скрытых слоев? В исследовательской литературе есть определенное соглашение в отношении того, что двух скрытых слоев достаточно почти для всех практических задач. В этой статье описывается подход к разработке улучшенных моделей для прогнозирования цен с использованием глубоких нейронных сетей. В основе него лежит способность глубоких сетей изучать абстрактные характеристики на основе необработанных данных. Предварительные результаты подтверждают, что наша глубокая сеть обеспечивает значительно более высокую точность прогнозов, чем базовые модели для развитых валютных рынков.
Доброго дня, хабровчане!
В этой статье поговорим о такой современной, модной и очень важной теме как машинное обучение и нейронные сети. О важности этой темы я писать не буду — каждый день об этом говорят по телевизору и пишут в газетах интернетах.
Тема распознавания цифр на основе данных от MNIST уже достаточно заезженная, но я все-таки поделюсь с Вами своим первым опытом в этой области, который привел к определенному результату, правдивость и достоверность которого Вы сможете сами проверить.
Итак, о чем же эта статья?!
В статье я буду с нуля реализовывать классический алгоритм обратного распространения ошибки с помощью последовательного (стохастического) режима обучения, чтобы обучить многослойный персептрон (полносвязную нейронную сеть прямого распространения) распознавать рукописные цифры. Каждый свой шаг я буду стараться подробно комментировать, ссылаясь на книгу Хайкина по нейросетям. Я думаю многие, кто занимается машинным обучением профессионально, начинали свой путь именно с этой книги, ну или хотя пару раз заглянули в нее.
Как Вы заметили, эта статья является переводом. Я бы сказал — частичным переводом, так как в оригинальной статье автор пишет алгоритм на языке C. Я же буду писать на C#.
К тому же, точность предсказаний обученной нейросети зависит от очень большого количества параметров: архитектура сети, количество скрытых слоев, количество нейронов на слоях, функции активации, инициализация весовых коэффициентов и т.д. Поэтому в качестве упрощения я буду идти примерно той же дорогой, как и автор оригинала для ускоренного получения более-менее приемлемого результата.
От Вас требуются элементарные знания высшей математики (знать что такое сумма от 1 до n, что такое функция и производная) ну и иметь хотя бы какое-то общее представление о нейронных сетях. Было бы неплохо прочесть первую главу Хайкина.
Содержание
-
Обучающая выборка. MNIST
-
Результаты
-
Модель нейрона
-
Модель слоя
-
Модель нейронной сети
-
Подготовка обучающей выборки. Архитектура сети
-
Инициализация нейросети
-
Прямой проход
-
Алгоритм обратного распространения
-
Заключение
Обучающая выборка. MNIST
Для тех, кто первый раз видит эту аббревиатуру. В качестве данных для обучения я использовал изображения рукописных цифр из базы данных MNIST. Скачать бинарные файлы с изображениями и метками можно здесь. Но на всякий случай я загрузил их в свой проект.
Пару слов про эти бинарники. Файлы train-images-idx3-ubyte
и train-labels-idx1-ubyte
содержат в себе 60К изображений и меток (наименование цифры) в закодированном виде, соответственно. Эта выборка будет служить для обучения нейросети и корректировки весовых коэффициентов. Файлы t10k-images-idx3-ubyte
и t10k-labels-idx1-ubyte
содержат 10К картинок и меток, соответственно, для тестирования уже обученной нейросети.
Для работы с этими файлами я использовал библиотеку MNIST.IO, которая отлично справилась со своей задачей. Хотя для тренировки нейросети это и не нужно, но мне все-таки захотелось взглянуть на эти картинки «вживую», поэтому я раскодировал все изображения и сохранил их в соответствующие папки проекта. Здесь находятся 60К из тренировочной выборки, а тут 10К для тестирования.
Каждая картинка имеет размер 28*28 пикселей и представляет из себя черно-белое изображение рукописной цифры от 0 до 9.
Результаты
Пожалуй, начну сразу с результатов. Результатом является обученный персептрон (784-80-10), т.е. нейросеть у которой 784 входа, один единственный скрытый слой с 80-ю нейронами и 10 выходных нейронов. При тестировании на выборке из 10К он показал точность распознавания % (т.е. он удачно распознал 9572 цифры из 10К), остальные 428 ему распознать не удалось. Их я решил сохранить в отдельную папку (в названии каждой картинки три числа: порядковый номер, ожидаемый ответ, ответ от нейросети). Обучал в 16 эпох (16*60К изображений) с параметров обучения =
.
Уже готовые, скорректированные весовые коэффициенты скрытого и выходного слоя находятся здесь. Для экспериментов я создал песочницу с уже обученной моделью (на основе уже готовых коэффициентов) где на холсте размером 28 на 28 пикселей можно нарисовать свою цифру для распознавания и посмотреть на результат:
Модель нейрона
Модель нейрона в моей реализации имеет следующий вид:
public class Neuron
{
/// <summary>
/// Весовые коэффициенты (синаптические связи)
/// </summary>
public List<double> Weights { get; }
/// <summary>
/// Пороговое значение
/// </summary>
public double Bias { get; }
/// <summary>
/// Функция активации
/// </summary>
private Func<double, double> ActivationFunction { get; }
/// <summary>
/// Индуцированное локальное поле
/// </summary>
public double InducedLocalField { get; private set; }
/// <summary>
/// Локальный градиент
/// </summary>
public double LocalGradient { get; private set; }
}
Везде далее, если необходимо, я буду указывать в скобочках страницу и/или формулу и номер раздела в книге Хайкина, на основе которых я строил свою модель. Модель нейрона была взята со стр. 43, п. 1.3.
Что такое Weights, Bias и ActivationFunction думаю и так понятно. Свойство InducedLocalField будет использоваться как при прямом проходе, так и в методе обратного распространения (обратный проход) для корректировки весовых коэффициентов, — поэтому задавать его я буду уже при прямом проходе сигнала через сеть, чтобы на обратном пути брать уже вычисленное значение. LocalGradient необходим лишь на обратном проходе для корректировки весовых коэффициентов. Свойство ActivationFunction имеет функциональный тип данных, чтобы можно было подставлять разные функции активации.
Все-таки пару слов о Weights сказать нужно. На самом деле неправильно утверждать, что весовые коэффициенты принадлежат какому-то конкретному нейрону. Для тех кто знаком с графами — это как вес ребра, связывающего две соседние вершины графа, так и здесь, весовой коэффициент относится к синапсу, который связывает нейрон одного слоя с нейроном (либо входом) соседнего слоя.
Но в моей модели список Weights будет относиться к синапсам, которые связывают данный нейрон и нейроны предыдущего (расположенного левее) слоя. Поэтому это свойство включено в класс нейрона.
Забегая наперед, нужно отметить, что, в таком случае, для алгоритма обратного распространения нужно реализовать отдельный метод, который будет находить список весовых коэффициентов, относящихся к синапсам, связывающих данный нейрон и нейроны следующего за ним (расположенного правее) слоя. Я думаю, это станет понятней в дальнейшем в процессе реализации.
Функции активации (и их производные) хранятся отдельно:
ActivationFunctions.cs
public static class ActivationFunctions
{
/// <summary>
/// Параметр для сигмоидальной функции активации
/// </summary>
private const double a = 1.0;
/// <summary>
/// Пороговая функция активации (использовалась для тестирования и решения проблемы XOR)
/// </summary>
/// <param name="x">аргумент функции</param>
/// <returns></returns>
public static double ThresholdFunction(double x)
{
return x >= 0.0 ? 1.0 : 0.0;
}
/// <summary>
/// Возвращает значение сигмоидальной функции активации
/// </summary>
/// <param name="x">аргумент функции</param>
/// <returns></returns>
public static double SigmoidFunction(double x)
{
return 1.0 / (1.0 + Math.Pow(Math.E, -a * x));
}
/// <summary>
/// Возвращает значение производной сигмоидальной функции активации (для алгоритма обратного распространения)
/// </summary>
/// <param name="x">аргумент функции</param>
/// <returns></returns>
public static double SigmoidFunctionsDerivative(double x)
{
double factor = a * Math.Pow(Math.E, -a * x);
return factor * Math.Pow(SigmoidFunction(x), 2.0);
}
}
Модель слоя
public class Layer
{
/// <summary>
/// Нейроны
/// </summary>
public List<Neuron> Neurons { get; }
/// <summary>
/// Входной сигнал
/// </summary>
public List<double> InputSignals { get; set; }
}
Состоит из свойств Neurons — список нейронов данного слоя и InputSignals — входные сигналы, полученные от предыдущего слоя. InputSignals будут также сохраняться при прямом проходе, чтобы использовать их на обратном.
Модель нейронной сети
public class Network
{
/// <summary>
/// Скрытые слои
/// </summary>
public List<Layer> HiddenLayers { get; }
/// <summary>
/// Выходной слой
/// </summary>
public Layer OutputLayer { get; }
}
Модель нейросети состоит из списка скрытых HiddenLayers слоев и выходного слоя —OutputLayer.
Подготовка обучающей выборки. Архитектура сети
Для того чтобы запустить алгоритм обучения, необходимо каждый обучающий пример, а именно изображение с цифрой, преобразовать в сигнал, который будет скормлен нашему персептрону.
Каждое MNIST-изображение представляет из себя матрицу 28*28 пикселей со значениями в диапазоне от 0 до 255 (оттенок серого цвета). Но для наших целей мы поступим следующим образом — все белые пиксели (со значением 0) мы так и оставим нулем, а все остальные пиксели будем рассматривать как черный цвет, рисунок 2.
Белые пиксели остаются белыми, а все остальные будем считать черными.
После этого мы «расслоим» наше изображение:
и из полученных слоев составим вектор входного сигнала с набором из элементов. Белые и черные пиксели, будут представлять, соответственно, 0-ки и 1-ки.
Для этих целей был создан вспомогательный метод:
ConvertImageToFunctionSignal()
public static List<double> ConvertImageToFunctionSignal(byte[,] image)
{
List<double> functionSignal = new List<double>();
for (int i = 0; i < image.GetLength(0); i++)
for (int j = 0; j < image.GetLength(1); j++)
functionSignal.Add(image[i, j] == 0 ? 0.0 : 1.0);
return functionSignal;
}
Таким образом, у нас будет полносвязная нейронная сеть прямого распространения, состоящая из 784 входов, 1 скрытого слоя и выходного слоя с 10 нейронами. Все нейроны будут иметь сигмоидную функцию активации. Количество нейронов на скрытом слое будем варьировать для получения лучшего результата.
Инициализация нейросети
Как оказалось, не только необходимым, но и важным этапом в процессе обучения нейросети является инициализация ее весовых коэффициентов. Как пишет автор оригинала, нейросеть показала более высокую точность когда при ее инициализации всем весовым коэффициентам были присвоены рандомные значения от 0 до 1 с чередованием знаков. Т.е. каждый четный весовой коэффициент был положительным, а каждый нечетный — отрицательным. Тоже самое я решил применить не только к весам, но и к пороговым значениям (Bias), чтобы одна их половина была положительной, а другая — отрицательной.
А теперь — как это реализовано на практике.
В классе Network реализовано два конструктора. Один из них инициализирует всю сеть (весовые коэффициенты) рандомными значениями, а смысл другого конструктора в том, чтобы инициализировать сеть по уже скорректированным весовым коэффициентам, которые хранятся в текстовом файле.
Вот листинг кода первого конструктора с комментариями:
Network(). Конструктор для рандомной инициализации
/// <summary>
/// Инициализирует нейросеть с помощью входных параметров
/// </summary>
/// <param name="inputLayerDimension">количество входов нейросети</param>
/// <param name="outputLayerNeuronsCount">количество нейронов на выходном слое</param>
/// <param name="outputActivationFunction">функция активации у нейронов выходного слоя</param>
/// <param name="hiddenLayersDimensions">размерности скрытых слоев</param>
/// <param name="hiddenActivationFunctions">массив функций активаций нейронов скрытых слоев</param>
/// <param name="randomMinValue">левая граница для рандомных чисел</param>
/// <param name="randomMaxValue">правая граница для рандомных чисел</param>
public Network(int inputLayerDimension, int outputLayerNeuronsCount, Func<double, double> outputActivationFunction, int[] hiddenLayersDimensions = null,
Func<double, double>[] hiddenActivationFunctions = null, double randomMinValue = 0.0, double randomMaxValue = 1.0)
{
Random random = new Random();
// Если есть скрытые слои
if (hiddenLayersDimensions != null)
{
HiddenLayers = new List<Layer>();
// Сначала инициализируем первый скрытый слой
// Количество весовых коэффициентов у каждого нейрона первого скрытого слоя равно количеству нейронов входного слоя
HiddenLayers.Add(new Layer(CreateNeurons(hiddenLayersDimensions[0], inputLayerDimension, randomMinValue, randomMaxValue, hiddenActivationFunctions[0], random)));
// Если скрытых слоев больше 1
if (hiddenLayersDimensions.Length > 1)
{
// Количество весовых коэффициентов на втором и последующих скрытых слоях равно количеству нейронов на предыдущем скрытом слое
// Еще раз, первый скрытый слой уже проинициализирован, поэтому начинаем со второго (h = 1)
for (int h = 1; h < hiddenLayersDimensions.Length; h++)
HiddenLayers.Add(new Layer(CreateNeurons(hiddenLayersDimensions[h], hiddenLayersDimensions[h - 1], randomMinValue, randomMaxValue, hiddenActivationFunctions[h], random)));
}
}
// Если есть скрытые слои, то количество весовых коэффицинтов у нейронов выходного слоя равно количеству нейронов последнего скрытого слоя
// Если скрытых слоев нет, то количество весовых коэффицинтов у нейронов выходного слоя равно количеству входов сети
int outputWeightsCount = hiddenLayersDimensions != null ? hiddenLayersDimensions.Last() : inputLayerDimension;
OutputLayer = new Layer(CreateNeurons(outputLayerNeuronsCount, outputWeightsCount, randomMinValue, randomMaxValue, outputActivationFunction, random));
}
ну и соответствующие методы:
CreateNeurons()
/// <summary>
/// Возвращает список нейронов, инициализированных весовыми коэффициентами
/// </summary>
/// <param name="neuronsCount">количество нейронов</param>
/// <param name="weightsCount">количество весовых коэффициентов в каждом нейроне</param>
/// <param name="weightsMinValue">левая граница интервала случайных чисел</param>
/// <param name="weightsMaxValue">правая граница интервала случайных чисел</param>
/// <param name="activationFunction">функция активации</param>
/// <param name="random">экземпляр генератора случайных чисел</param>
/// <returns></returns>
private List<Neuron> CreateNeurons(int neuronsCount, int weightsCount, double weightsMinValue, double weightsMaxValue, Func<double, double> activationFunction, Random random)
{
List<Neuron> neurons = new List<Neuron>();
for (int i = 0; i < neuronsCount; i++)
{
List<double> weights = CreateRandomWeights(weightsCount, weightsMinValue, weightsMaxValue, random);
neurons.Add(new Neuron(activationFunction, weights, CreateRandomValue(random, weightsMinValue, weightsMaxValue, i)));
}
return neurons;
}
CreateRandomWeights()
/// <summary>
/// Возвращает список весовых коэффициентов инициализированных случайными значениями
/// </summary>
/// <param name="weightsCount">количество весовых коэффициентов</param>
/// <param name="minValue">левая граница интервала случайных чисел</param>
/// <param name="maxValue">правая граница интервала случайных чисел</param>
/// <param name="random">экземпляр генератора случайных чисел</param>
/// <returns></returns>
private List<double> CreateRandomWeights(int weightsCount, double minValue, double maxValue, Random random)
{
List<double> weights = new List<double>();
for (int i = 0; i < weightsCount; i++)
weights.Add(CreateRandomValue(random, minValue, maxValue, i));
return weights;
}
CreateRandomValue()
/// <summary>
/// Возвращает случайное число в заданном интервале
/// </summary>
/// <param name="random">экземпляр генератора случайных чисел</param>
/// <param name="minValue">левая граница интеравала</param>
/// <param name="maxValue">правая граница интеравала</param>
/// <param name="currentIndex">текущий индекс генерируемого значения</param>
/// <returns></returns>
private double CreateRandomValue(Random random, double minValue, double maxValue, int currentIndex)
{
double randomDouble = random.NextDouble() * (maxValue - minValue) + minValue;
if (currentIndex % 2 == 0) // Будем чередовать знаки через один
return -randomDouble;
return randomDouble;
}
Метод CreateRandomValue()
принимает аргумент currentIndex
, от которого, как было сказано выше, будет зависеть знак рандомного значения.
Второй конструктор просто принимает заранее подготовленные слои:
Network(). Конструктор для инициализации сети подготовленными слоями
public Network(List<Layer> HiddenLayers, Layer OutputLayer)
{
this.HiddenLayers = HiddenLayers;
this.OutputLayer = OutputLayer;
}
После обучения нейросети неплохо бы сохранить скорректированные весовые коэффициенты в файлы:
Запись весовых коэффициентов в файл
/// <summary>
/// Записывает данные по скрытым слоям (количество скрытых слоев, их размерности, пороговые значения, весовые коэффициенты) в файл
/// </summary>
/// <param name="fileName">имя файла для записи</param>
public void WriteHiddenWeightsToCSVFile(string fileName)
{
if (HiddenLayers == null)
return;
TextWriter textWriter = new StreamWriter(fileName);
textWriter.WriteLine(string.Format("{0};{1}", "hiddenLayersDimensions", string.Join(";", HiddenLayers.Select(x => x.Neurons.Count))));
foreach (Layer hiddenLayer in HiddenLayers)
foreach (Neuron neuron in hiddenLayer.Neurons)
textWriter.WriteLine("{0};{1}", neuron.Bias, string.Join(";", neuron.Weights));
textWriter.Close();
}
/// <summary>
/// Записывает пороговые значения и весовые коэффициенты выходного слоя сети в файл
/// </summary>
/// <param name="fileName">имя файла для записи</param>
public void WriteOutputWeightsToCSVFile(string fileName)
{
TextWriter textWriter = new StreamWriter(fileName);
foreach (Neuron neuron in OutputLayer.Neurons)
textWriter.WriteLine("{0};{1}", neuron.Bias, string.Join(";", neuron.Weights));
textWriter.Close();
}
Методы для чтения весовых коэффициентов вынесены в отдельный класс.
Прямой проход
Следующий метод класса Network запускает алгоритм прямого распространения сигнала по сети и возвращает выходной сигнал:
/// <summary>
/// Запускает алгоритм прямого распространения сигнала и возвращает ответ от сети
/// </summary>
/// <param name="functionSignal">функциональный сигнал (стимул), поступающий на вход нейросети</param>
/// <returns></returns>
public List<double> MakePropagateForward(List<double> functionSignal)
{
// Если имеются скрытые слои, то передаем сигнал по скрытым слоям
if (HiddenLayers != null)
foreach (Layer hiddenLayer in HiddenLayers)
functionSignal = SetInputSignalAndInducedLocalFieldAndReturnOutputSignal(hiddenLayer, functionSignal);
// Возвращаем сигнал от выходного слоя
return SetInputSignalAndInducedLocalFieldAndReturnOutputSignal(OutputLayer, functionSignal);
}
В этом методе входной сигнал, который представляет из себя вектор-изображение из 784 элементов, передается сначала по скрытым слоям (если они есть), а затем от выходного слоя возвращается сигнал из 10 значений. Каждое из этих 10 значений представляет собой вероятность предсказания той или иной цифры.
В процессе передачи сигнала от слоя к слою метод с длинным названием также устанавливает свойства InputSignals класса Layer и InducedLocalField класса Neuron:
/// <summary>
/// Задает слою входной сигнал, устанавливает локальные индуцированные поля нейронов и возвращает выходной сигнал
/// </summary>
/// <param name="layer">слой</param>
/// <param name="functionSignal">входной сигнал</param>
/// <returns>выходной сигнал</returns>
private List<double> SetInputSignalAndInducedLocalFieldAndReturnOutputSignal(Layer layer, List<double> functionSignal)
{
layer.InputSignals = functionSignal;
foreach (Neuron neuron in layer.Neurons)
neuron.SetInducedLocalField(functionSignal);
return layer.ProduceSignals();
}
Алгоритм обратного распространения
Как было сказано в самом начале статьи, реализовывать я буду последовательный (стохастический) режим обучения:
Описание последовательного режима обучения (гл. 4, раздел 4.3)
Процесс обучения начинается с метода Train()
класса Network:
Train()
/// <summary>
/// Запускает алгоритм обучения нейронной сети
/// </summary>
/// <param name="imagesFileName">путь к бинарному файлу MNIST с изображениями</param>
/// <param name="labelsFileName">путь к бинарному файлу MNIST с метками (наименованиями цифр)</param>
/// <param name="learningRateParameter">параметр скорости обучения</param>
/// <param name="numberOfEpochs">количество эпох</param>
public void Train(string imagesFileName, string labelsFileName, double learningRateParameter, int numberOfEpochs)
{
for (int e = 0; e < numberOfEpochs; e++)
{
// Получаем изображения
IEnumerable<TestCase> testCases = FileReaderMNIST.LoadImagesAndLables(labelsFileName, imagesFileName);
foreach (TestCase test in testCases)
{
// Конвертируем изображение в функциональный сигнал
List<double> functionSignal = ImageHelper.ConvertImageToFunctionSignal(test.Image);
// Получаем ожидаемый ответ
List<double> desiredResponse = GetDesiredResponse(test.Label);
// Получаем ответ от сети (прямой проход)
List<double> outputSignal = MakePropagateForward(functionSignal);
// Вычисляем сигнал ошибки как разность между ожидаемым и фактическим ответом нейросети
List<double> errorSignal = GetErrorSignal(desiredResponse, outputSignal);
// Запускаем алгоритм обратного распространения ошибки
MakePropagateBackward(errorSignal, learningRateParameter);
}
Console.WriteLine("epoch " + e.ToString() + " finished"); // Выводим в консоль прогресс выполнения
}
}
который содержит в себе метод для запуска алгоритма обратного распространения:
/// <summary>
/// Запускает алгоритм обратного распространения ошибки
/// </summary>
/// <param name="errorSignal">сигнал ошибки</param>
/// <param name="learningRateParameter">параметр скорости обучения</param>
private void MakePropagateBackward(List<double> errorSignal, double learningRateParameter)
{
// Вычисляем локальные градиенты для выходного слоя
OutputLayer.CalculateAndSetLocalGradients(errorSignal);
// Корректируем весовые коэффициенты и пороговые значения выходного слоя
OutputLayer.AdjustWeightsAndBias(learningRateParameter);
// Если скрытых слоев нет, то заканчиваем процесс
if (HiddenLayers == null)
return;
// Устанавливаем выходной слой как предыдущий
// (для обратного прохода это тот слой, который расположен правее)
Layer previousLayer = OutputLayer;
// Идем от последнего скрытого слоя к первому
for (int i = HiddenLayers.Count - 1; i >= 0; i--)
{
// Вычисляем локальные градиенты
HiddenLayers[i].CalculateAndSetLocalGradients(previousLayer);
// Корректируем весовые коэффициенты и пороговые значения
HiddenLayers[i].AdjustWeightsAndBias(learningRateParameter);
previousLayer = HiddenLayers[i];
}
}
По комментариям я думаю понятно, что тут происходит. Опишу входящие сюда методы подробней.
Зная сигнал ошибки, мы можем вычислить локальные градиенты для нейронов выходного слоя:
/// <summary>
/// Вычисляет и устанавливает нейронам ВЫХОДНОГО слоя локальные градиенты
/// </summary>
/// <param name="errorSignal">сигнал ошибки</param>
internal void CalculateAndSetLocalGradients(List<double> errorSignal)
{
for (int i = 0; i < Neurons.Count; i++)
Neurons[i].SetLocalGradient(errorSignal[i] * ActivationFunctions.SigmoidFunctionsDerivative(Neurons[i].InducedLocalField));
}
по формуле:
Вычислив локальные градиенты выходного слоя — корректируем весовые коэффициенты и пороговые значения нейронов выходного слоя:
/// <summary>
/// Корректирует весовые коэффициенты и пороговое значение
/// </summary>
/// <param name="learningRateParameter">параметр скорости обучения</param>
/// <param name="inputSignals">входной сигнал нейрона, заданный при прямом проходе</param>
internal void AdjustWeightsAndBias(double learningRateParameter, List<double> inputSignals)
{
for (int i = 0; i < Weights.Count; i++)
{
Weights[i] += learningRateParameter * LocalGradient * inputSignals[i]; // Корректируем весовые коэффициенты
Bias += learningRateParameter * LocalGradient; // Корректируем пороговое значение (inputSignal для него равен 1)
}
}
по формуле:
Затем вычисляем локальные градиенты скрытых слоев:
/// <summary>
/// Вычисляет и устанавливает нейронам СКРЫТОГО слоя локальные градиенты на основе предыдущего слоя в алгоритме обратного распространения
/// </summary>
/// <param name="previousLayer">предыдущий слой (расположенный правее текущего)</param>
internal void CalculateAndSetLocalGradients(Layer previousLayer)
{
for (int i = 0; i < Neurons.Count; i++)
{
// Получаем весовые коэффициенты, непосредственно связанный с этим нейроном
List<double> associatedWeights = GetAssociatedWeights(previousLayer, i);
// Подсчитываем внутреннюю сумму
double innerSum = GetInnerSum(associatedWeights, previousLayer);
// Вычисляем и устанавливаем нейрону скрытого слоя локальный градиент
Neurons[i].SetLocalGradient(innerSum * ActivationFunctions.SigmoidFunctionsDerivative(Neurons[i].InducedLocalField));
}
}
по формуле:
После того, как локальные градиенты нейронов скрытого слоя вычислены, по формуле (4.25) корректируем весовые коэффициенты и пороговые значения.
Данные вычисления проводятся для всех слоев в обратном направлении. После того, как все весовые коэффициенты и пороговые значения откорректированы, подаем на вход новый обучающий пример, находим сигнал ошибки и запускаем новую итерацию алгоритма обратного распространения ошибки.
Заключение
В результате экспериментов было установлено, что нейросеть неплохо обучается, если на обратном проходе не корректировать пороговые значения, т.е. закомментировать вот эту строчку:
internal void AdjustWeightsAndBias(double learningRateParameter, List<double> inputSignals)
{
for (int i = 0; i < Weights.Count; i++)
{
Weights[i] += learningRateParameter * LocalGradient * inputSignals[i]; // Корректируем весовые коэффициенты
// Bias += learningRateParameter * LocalGradient; // Корректируем пороговое значение (inputSignal для него равен 1)
}
}
В общем, как бы там ни было, обучение нейросети — процесс очень творческий и неоднозначный и требует довольно долгих расчетов и экспериментов. И, как утверждает автор оригинальной статьи, вместо тюнинга параметров нейросети и достижения более точных результатов лучше перейти к реализации принципиально нового типа сетей — сверточных нейронных сетей. Но об этом, наверное, в другой раз.
В статье я почти ни слова не сказал о производительности, хотя это и не является главной темой работы. Скажу лишь, что на моем Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz 1.80GHz c 16 ГБ ОЗУ процесс обучения с одним скрытым слоем (80 нейронов) в 16 эпох занял примерно 40 мин (да, не быстро!). Поэтому оставляю за читателем право максимально распараллелить все возможные процессы (Parallel.For, PLINQ и т.п. вам в помощь).
Надеюсь Вам была полезна моя статья!
С наступающим Рождеством!
Мира, добра и чистого кода!