Хабрахабр

[Перевод] Нейросети и глубокое обучение: онлайн-учебник, глава 1

Примечание

Michael NielsenПеред вами – перевод свободной онлайн-книги Майкла Нильсена «Neural Networks and Deep Learning», распространяемой под лицензией Creative Commons Attribution-NonCommercial 3.0 Unported License. Мотивацией к его созданию послужил успешный опыт перевода учебника по программированию, "Выразительный JavaScript". Книга по нейросетям тоже достаточно популярна, на неё активно ссылаются авторы англоязычных статей. Её переводов я не нашёл, за исключением перевода начала первой главы с сокращениями.

Для поддержки переводчика на Хабре есть форма «поддержать автора». Желающие отблагодарить автора книги могут сделать это на её официальной странице, переводом через PayPal или биткоин.

Введение

Этот учебник подробно расскажет вам о таких понятиях, как:

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

Нейросети (НС) и глубокое обучение (ГО) на сегодня дают наилучшее решение многих задач из областей распознавания изображений, голоса и обработки естественного языка. Этот учебник научит вас многим ключевым концепциям, лежащим в основе НС и ГО.

О чём эта книга

НС – одна из прекраснейших программных парадигм, когда-либо придуманных человеком. При стандартном подходе к программированию мы сообщаем компьютеру, что делать, разбиваем большие задачи на множество малых, точно определяем задачи, которые компьютеру будет легко исполнить. В случае с НС мы, наоборот, не говорим компьютеру, как решать задачу. Он сам обучается этому на основе «наблюдений» за данными, «придумывая» собственное решение поставленной задачи.

Однако до 2006 года мы не знали, как обучать НС так, чтобы они могли превзойти более традиционные подходы, за исключением нескольких особых случаев. Автоматическое обучение на основе данных звучит многообещающе. глубоких нейросетей (ГНС). В 2006 были открыты техники обучения т.н. Их продолжали разрабатывать, и сегодня ГНС и ГО достигли потрясающих результатов во многих важных задачах, связанных с компьютерным зрением, распознаванием речи и обработки естественного языка. Теперь эти техники известны, как глубокое обучение (ГО). В крупных масштабах их развёртывают такие компании, как Google, Microsoft и Facebook.

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

Подход с упором на принципы

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

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

Практический подход

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

Но вам не обязательно быть профессиональным программистом. Такой практический подход означает, что вам потребуется некий опыт в программировании. 7), который должен быть понятным, даже если вы не писали программ на python. Я написал код на python (версии 2. Весь код можно скачать по ссылке. В процессе изучения мы создадим свою библиотечку для НС, которую вы сможете использовать для экспериментов и дальнейшего обучения. Закончив книгу, или в процессе чтения, вы сможете выбрать одну из более завершённых библиотек для НС, приспособленных для использования в настоящих проектах.

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

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

Упражнения и задачи

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

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

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

Они сложнее упражнений, и с некоторыми вам придётся тяжело. Задачи – дело другое. Это раздражает, но, конечно, терпение перед лицом такого разочарования – это единственный способ по-настоящему понять и усвоить предмет.

Ещё лучше – подобрать собственный проект. Так что я не рекомендую решать все задачи. Или для предсказания стоимости акций. Может, вы захотите использовать НС для классификации своей коллекции музыки. Но найдите интересный вам проект. Или чего-то ещё. Проблемы с собственным проектом научат вас большему, чем работа с любым количеством задач. И потом вы сможете игнорировать задачи из книги, или использовать их чисто как вдохновение для работы над вашим проектом. Эмоциональное вовлечение – ключевой фактор в достижении мастерства.

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

Глава 1

Зрительная система человека – одно из чудес света. Рассмотрим следующую последовательность рукописных цифр:

Но эта простота обманчива. Большинство людей без труда прочтут их, как 504192. При этом в зрении человека участвует не только V1, но целая последовательность участков мозга — V2, V3, V4 и V5 – которые занимаются всё более сложной обработкой изображений. В каждом полушарии мозга у человека есть первичная зрительная кора, также известная, как V1, в которой содержится 140 млн нейронов и десятки миллиардов связей между ними. Распознавать рукописные цифры не так-то легко. Мы носим в своей голове суперкомпьютер, настроенный эволюцией в течение сотен миллионов лет, и прекрасно адаптированный для понимания видимого мира. Но почти вся эта работа проводится бессознательно. Просто мы, люди, потрясающе, удивительно хорошо распознаём то, что показывают нам наши глаза. И обычно мы не придаём значения тому, какую сложную задачу решают наши зрительные системы.

То, что кажется лёгким в нашем исполнении, внезапно оказывается чрезвычайно сложным. Сложность распознавания зрительных закономерностей становится очевидной, когда вы пытаетесь написать компьютерную программу для распознавания таких цифр, как приведённые выше. Пытаясь сформулировать эти правила чётко, вы быстро вязнете в трясине исключений, подводных камней и особых случаев. Простое понятие о том, как мы распознаём формы – «у девятки сверху петля, и вертикальная чёрточка справа внизу»- оказывается вовсе не таким простым для алгоритмического выражения. Задача кажется безнадёжной.

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

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

Программа будет всего в 74 строки длиной, и не будет использовать специальных библиотек для НС. В этой главе мы напишем компьютерную программу, реализующую НС, обучающуюся распознавать рукописные цифры. Более того, в дальнейших главах мы разработаем идеи, способные улучшить точность до 99% и более. Однако эта короткая программа сможет распознавать рукописные цифры с точностью более 96%, не требуя вмешательства человека. На самом деле лучшие коммерческие НС настолько хорошо справляются с задачей, что используются банками для обработки чеков, и почтовой службой – для распознавания адресов.

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

Однако в процессе мы выработаем много ключевых идей, связанных с НС, включая два важных типа искусственного нейрона (перцептрон и сигмоидный нейрон), и стандартный алгоритм обучения НС, стохастический градиентный спуск. Конечно, если бы целью этой главы было только написать программу распознавания рукописных цифр, то глава была бы куда короче! Для этого требуется более долгий разговор, чем если бы я просто представил базовую механику происходящего, однако это стоит более глубокого понимания, которое возникнет у вас. В тексте я концентрируюсь на том, чтобы объяснить, почему всё делается именно так, и на формировании вашего понимания НС. Среди других преимуществ – к концу главы вы поймёте, что такое ГО и почему оно так важно.

Перцептроны

Что такое нейросеть? Для начала я расскажу об одном типе искусственного нейрона, который называется перцептрон. Перцептроны придумал в 1950-60-х учёный Фрэнк Розенблатт, вдохновившись ранней работой Уоррена Мак-Каллока и Уолтера Питтса. Сегодня чаще используются другие модели искусственных нейронов – в данной книге, и большинстве современных работ по НС в основном используют сигмоидную модель нейрона. Мы вскоре с ней познакомимся. Но чтобы понять, почему сигмоидные нейроны определяются именно так, стоит потратить время на разбор перцептрона.

Перцептрон принимает на вход несколько двоичных чисел x1,x2,… и выдаёт одно двоичное число: Так как же работают перцептроны?

В общем случае их может быть больше или меньше. В данном примере у перцептрона есть три числа на входе, x1, x2, x3. Он ввёл веса, w1, w2, вещественные числа, выражающие важность соответствующих входных чисел для результатов. Розенблатт предложил простое правило для вычисления результата. Как и веса, порог – вещественное число, параметр нейрона. Выход нейрона, 0 или 1, определяется тем, меньше или больше некоего порога [threshold] взвешенная сумма $\sum_j w_jx_j$. Говоря математическими терминами:

$ output = \begin 0 ~ if ~ \sum_j w_jx_j \leq threshold \\ 1 ~ if ~ \sum_j w_jx_j > threshold \end{cases} \tag{1} $

Вот и всё описание работы перцептрона!

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

  1. Хорошая ли погода?
  2. Хочет ли идти с вами ваш партнёр?
  3. Далеко ли фестиваль от остановки общественного транспорта? (Машины у вас нет).

Три этих фактора можно представить двоичными переменными x1, x2, x3. К примеру, x1 = 1, если погода хорошая, а 0 – если плохая. x2 = 1, если ваш партнёр хочет пойти, и 0 – если нет. То же для x3.

Но, возможно, вы просто ненавидите плохую погоду, и в случае непогоды на фестиваль не пойдёте. Теперь, допустим, вы фанатеете от сыра настолько, что готовы ехать на фестиваль, даже если вашего партнёра это не интересует, и до него трудно добраться. Один из способов – выбрать вес w1 = 6 для погоды, и w2 = 2, w3 = 2 для других условий. Вы можете использовать перцептроны для моделирования такого процесса принятия решения. Наконец, допустим, вы выбираете для перцептрона порог 5. Большее значение w1 говорит о том, что погода имеет для вас значение гораздо большее, чем то, присоединится ли к вам ваш партнёр, или близость фестиваля к остановке. Желание партнёра и близость остановки не влияют на выходное значение. С такими вариантами перцептрон реализует нужную модель принятия решений, выдавая 1, когда погода хорошая, и 0, когда она плохая.

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

Но этот пример показывает, как перцептрон может взвешивать разные виды свидетельств, чтобы принимать решения. Очевидно, перцептрон не является полной моделью принятия решений человеком! Кажется возможным, что сложная сеть перцептронов может принимать весьма сложные решения:

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

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

Условие $\sum_j w_jx_j > treshold$ неуклюжее, и мы можем договориться о двух изменениях записи для её упрощения. Давайте упростим описание перцептронов. Второе – перенести порог в другую часть неравенства, и заменить его значением, известным, как смещение перцептрона [bias], $b \equiv −threshold$. Первое – записывать $\sum_j w_jx_j$ как скалярное произведение, $w \cdot x = \sum_j w_jx_j$, где w и x – вектора, компонентами которых служат веса и входные данные, соответственно. Используя смещение вместо порога, мы можем переписать правило перцептрона:

$ output = \begin{cases} 0 ~ if ~ w \cdot x + b \leq 0 \\ 1 ~ if ~ w \cdot x + b > 0 \end{cases} \tag{2} $

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

Ещё один метод их использования – вычисление элементарных логических функций, которые мы обычно считаем основными вычислениями, таких, как AND, OR и NAND. Я описал перцептроны с точки зрения метода взвешивания свидетельств с целью принятия решения. Вот он: Допустим, к примеру, что у нас есть перцептрон с двумя входами, вес каждого из которых равен -2, а его смещение равно 3.

Те же вычисления говорят, что входные данные 01 и 10 дают 1. Входные данные 00 дают на выходе 1, поскольку (−2)∗0+(−2)∗0+3=3 больше нуля. Поэтому наш перцептрон реализует функцию NAND! Но 11 на входе даёт 0 на выходе, поскольку (−2)∗1+(−2)∗1+3=−1, меньше нуля.

На самом деле, мы можем использовать сети перцептронов для вычисления вообще любых логических функций. Этот пример показывает, что можно использовать перцептроны для вычисления базовых логических функций. К примеру, можно использовать вентили NAND для создания контура, складывающего два бита, x1 и x2. Дело в том, что логический вентиль NAND универсален для вычислений – на его основе можно строить любые вычисления. Для этого нужно вычислить побитовую сумму $ x_1 \oplus x_2 $, а также флаг переноса, который равняется 1, когда оба x1 и x2 равны 1 – то есть, флаг переноса является просто результатом побитового умножения x1x2:

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

Определяя модель перцептрона, я не упоминал о допустимости подобной схемы двойного выхода в одно и то же место. Один примечательный аспект данной сети из перцептронов – выходные данные самого левого из них используются дважды как входные для нижнего. Если мы не хотим позволять такое, мы можем просто объединить две линии с весами в -2 в одну с весом -4. На самом деле, это не имеет большого значения. После этого изменения сеть выглядит следующим образом, причём все неразмеченные веса равны -2, все смещения равны 3, а один вес -4 отмечен: (Если это не кажется вам очевидным, остановитесь, и докажите это себе).

Такая запись перцептронов, у которых есть выход, но нет входов:

Это не значит, что у него нет входов. является просто сокращением. Тогда взвешенная сумма ∑jwjxj была бы всегда равной нулю, поэтому перцептрон выдавал бы 1 при b > 0 и 0 при b ≤ 0. Чтобы понять это, допустим, что у нас есть перцептрон без входов. Лучше считать входные перцептроны не перцептронами, а особыми единицами, которые просто определены так, чтобы выдавать нужные значения x1, x2,… То есть, перцептрон просто выдавал бы фиксированное значение, а не необходимое нам (x1 в примере выше).

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

Обнадёживает она, гарантируя, что сеть из перцептронов может быть настолько же мощной, насколько любое другое вычислительное устройство. Вычислительная универсальность перцептронов одновременно обнадёживает и разочаровывает. Так себе открытие! Разочаровывает, создавая впечатление, что перцептроны – всего лишь новый тип логического вентиля NAND.

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

Сигмоидные нейроны

Обучающие алгоритмы – это прекрасно. Однако как разработать такой алгоритм для нейросети? Допустим, у нас есть сеть перцептронов, которую мы хотим использовать для обучения решения задачи. Допустим, входными данными сети могут быть пиксели отсканированного изображения рукописной цифры. И мы хотим, чтобы сеть узнала веса и смещения, необходимые для правильной классификации цифры. Чтобы понять, как может работать такое обучение, представим, что мы немного меняем некий вес (или смещение) в сети. Мы хотим, чтобы это небольшое изменение привело к небольшому изменению выходных данных сети. Как мы скоро видим, это свойства делает возможным обучение. Схематично мы хотим следующего (очевидно, такая сеть слишком проста, чтобы распознавать рукописный ввод!):

К примеру, допустим, что сеть неправильно отнесла изображение к «8», хотя должна была к «9». Если бы небольшое изменение веса (или смещения) приводило бы к небольшому изменению результата на выходе, мы могли бы изменять веса и смещения, чтобы наша сеть вела себя чуть ближе к желаемому. А потом мы бы повторяли это, изменяя веса и смещения снова и снова, чтобы получать всё лучший и лучший результат. Мы могли бы разобраться, как внести небольшое изменение в веса и смещения, чтобы сеть немного ближе подобралась к классификации изображения, как «9». Сеть бы училась.

Небольшое изменение весов или смещения любого перцептрона иногда может привести к изменению его выхода на противоположный, допустим, с 0 на 1. Проблема в том, что если в сети есть перцептроны, такого не происходит. И даже если теперь наша «9» и будет правильно распознана, поведение сети со всеми остальными изображениями, вероятно, полностью изменилось таким образом, который сложно контролировать. Такой переворот может изменить поведение остальной сети очень сложным образом. Возможно, существует некий хитроумный способ обойти эту проблему. Из-за этого сложно представить, как можно постепенно подстраивать веса и смещения, чтобы сеть постепенно приближалась к желаемому поведению. Но нет никакого простого решения задачи обучения сети из перцептронов.

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

Рисовать мы их будем так же, как перцептроны: Давайте я опишу сигмоидный нейрон.

К примеру, величина 0,638 будет допустимым значением входных данных для сигмоидного нейрона (СН). У него точно так же есть входные данные x1, x2,… Но вместо того, чтобы приравниваться к 0 или 1, эти входы могут иметь любое значение в промежутке от 0 до 1. Но его выходным значением будет не 0 или 1. Так же, как у перцептрона, у СН есть веса для каждого входа, w1, w2,… и общее смещение b. Это будет σ(w⋅x+b), где σ — это сигмоида.

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

Определяется функция так:

$ \sigma(z) \equiv \frac{1}{1+e^{-z}} \tag{3} $

В нашем случае выходное значение сигмоидного нейрона с входными данными x1, x2,… весами w1, w2,… и смещением b будет считаться, как:

$ \frac{1}{1+exp(-\sum_j w_jx_j - b)} \tag{4} $

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

Тогда e – z ≈ 0, поэтому σ(z) ≈ 1. Чтобы понять сходство с моделью перцептрона, допустим, что z ≡ w ⋅ x + b – большое положительное число. Допустим, что z = w ⋅ x + b большое со знаком минус. Иначе говоря, когда z = w ⋅ x + b большое и положительное, выход СН примерно равен 1, как у перцептрона. Так что при больших z со знаком минус поведение СН тоже приближается к перцептрону. Тогда e – z → ∞, а σ(z) ≈ 0. И только когда w ⋅ x + b имеет средний размер, наблюдаются серьёзные отклонения от модели перцептрона.

Как нам его понять? Что насчёт алгебраического вида σ? Вот она: На самом деле, точная форма σ не так уж важна – важна форма функции на графике.

Это сглаженный вариант ступенчатой функции:

Если бы σ была ступенчатой, тогда СН был бы перцептроном, поскольку у него на выходе наблюдались бы 0 или 1 в зависимости от знака w ⋅ x + b (ну, на самом деле при z = 0 перцептрон выдаёт 0, а ступенчатая функция – 1, так что в одной этой точке функцию пришлось бы поменять).

И главным тут является гладкость функции, а не её точная форма. Используя реальную функцию σ, мы получаем сглаженный перцептрон. Алгебра говорит нам, что Δoutput хорошо аппроксимируется так: Гладкость означает, что небольшие изменения Δwj весов и δb смещений дадут небольшие изменения Δoutput выходных данных.

$ \Delta output \approx \sum_j \frac{\partial output}{\partial w_j}\Delta w_j + \frac{\partial output}{\partial b}\Delta b \tag{5} $

Не паникуйте, если чувствуете себя неуверенно в компании частных производных! Где суммирование идёт по всем весам wj, а ∂output/∂wj и ∂output/∂b обозначают частные производные выходных данных по wj и b соответственно. Её линейность облегчает выбор небольших изменений весов и смещений для достижения любого желаемого небольшого смещения выходных данных. Хотя формула и выглядит сложной, со всеми этими частными производными, на самом деле она говорит нечто совсем простое (и полезное): Δoutput – это линейная функция, зависящая от Δwj и Δb весов и смещения. Так что, хотя по качественному поведению СН похожи на перцептроны, они облегчают понимание того, как можно изменить выход, меняя веса и смещения.

На самом деле позднее мы иногда будем рассматривать нейроны, выход которых равняется f(w ⋅ x + b), где f() – некая другая функция активации. Если для нас имеет значение общая форма σ, а не её точный вид, то почему мы используем именно такую формулу (3)? Оказывается, что когда мы потом подсчитываем эти частные производные, использование σ сильно упрощает алгебру, поскольку у экспонент есть очень приятные свойства при дифференцировании. Главное, что меняется при смене функции – значения частных производных в уравнении (5). В любом, случае, σ часто используется в работе с нейросетями, и чаще всего в этой книге мы будем использовать такую функцию активации.

Очевидно, главным различием между перцептронами и СН будет то, что СН не выдают только 0 или 1. Как интерпретировать результат работы СН? Это может быть полезно, к примеру, если вам нужно, чтобы выходное значение обозначало, к примеру, среднюю яркость пикселей изображения, поступившего на вход НС. Их выходными данными может быть любое вещественное число от 0 до 1, так что значения типа 0,173 или 0,689 являются допустимыми. Допустим, мы хотим, чтобы выход сети говорил о том, что «на вход поступило изображение 9» или «входящее изображение не 9». Но иногда это может быть неудобно. Но на практике мы можем договориться, что любое выходное значение не меньше 0,5 обозначало бы «9» на входе, а любое значение меньше 0,5, обозначало бы, что это «не 9». Очевидно, проще было бы, если бы выходные значения были 0 или 1, как у перцептрона. Я всегда буду явно указывать на наличие подобных договорённостей.

Упражнения

  • СН, симулирующие перцептроны, часть 1

Допустим, мы берём все веса и смещения из сети перцептронов, и умножаем их на положительную константу c>0. Покажите, что поведение сети не изменяется.

  • СН, симулирующие перцептроны, часть 2

Допустим, у нас та же ситуация, что в прошлой задаче – сеть перцептронов. Также допустим, что выбраны входные данные для сети. Конкретное значение нам не нужно, главное, что оно фиксировано. Допустим, веса и смещения таковы, что w⋅x+b ≠ 0, где x – входное значение любого перцептрона сети. Теперь заменим все перцептроны в сети на сН, и умножим веса и смещения на положительную константу c>0. Покажите, что в пределе c → ∞ поведение сети из СН будет ровно таким же, как сети перцептронов. Каким образом это утверждение будет нарушаться, если для одного из перцептронов w⋅x+b = 0?

Архитектура нейросетей

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

Самый правый, или выходной слой, содержит выходные нейроны, или, как в нашем случае, один выходной нейрон. Как я уже упоминал, самый левый слой в сети называется входным слоем, а его нейроны – входными нейронами. Термин «скрытый», возможно, звучит немного загадочно – впервые услышав его, я решил, что у него должна быть некая глубокая философская или математическая важность – однако он означает лишь «не вход и не выход». Средний слой называется скрытым, поскольку его нейроны не являются ни входными, ни выходными. К примеру, в следующей четырёхслойной сети есть два скрытых слоя: У сети выше есть только один скрытый слой, но у некоторых сетей есть по нескольку скрытых слоёв.

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

К примеру, допустим, мы пытаемся определить, означает ли рукописная цифра «9», или нет. Проектирование входных и выходных слоёв иногда является простой задачей. Если изображение будет чёрно-белым, размером 64х64 пикселя, тогда у нас будет 64х64=4096 входных нейронов, с яркостью в промежутке от 0 до 1. Естественной схемой сети будет кодировать яркость пикселей изображения во входных нейронах. Выходной слой будет содержать всего один нейрон, чьё значение менее 0,5 будет означать, что «на входе была не 9», а значения более будут означать, что «на входе была 9».

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

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

Эти модели называются рекуррентными нейронными сетями (РНС). Однако существуют и другие модели искусственных НС, в которых возможно использовать петли обратной связи. Эта активация может стимулировать другие нейтроны, которые могут активироваться чуть позже, также на ограниченное время. Идея этих сетей в том, что их нейроны активируются на ограниченные промежутки времени. Петли в таких моделях не представляют проблем, поскольку выход нейрона влияет на его вход в некий более поздний момент, а не сразу. Это приводит к активации следующих нейронов, и со временем мы получаем каскад активированных нейронов.

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

Простая сеть классификации рукописных цифр

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

на шесть отдельных

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

это 5.

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

Для распознавания отдельных цифр мы будем использовать НС из трёх слоёв:

Как будет указано в следующем разделе, наши обучающие данные будут состоять из множества изображений отсканированных рукописных цифр размера 28х28 пикселей, поэтому входной слой содержит 28х28=784 нейрона. Входной слой сети содержит нейроны, кодирующие различные значения входных пикселей. Входящие пиксели чёрно-белые, при этом значение 0,0 обозначает белый цвет, 1,0 обозначает чёрный, а промежуточные значения обозначают всё более тёмные оттенки серого. Для простоты я не стал указывать большую часть из 784 нейронов на диаграмме.

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

Если активируется первый нейрон, то есть, его выходное значение ≈ 1, это говорит о том, что сеть считает, что на входе был 0. В выходном слое сети 10 нейронов. И так далее. Если активируется второй нейрон, сеть считает, что на входе был 1. Если это, допустим, нейрон №6, тогда наша сеть считает, что на входе была цифра 6. Строго говоря, мы нумеруем выходные нейроны от 0 до 9, и смотрим, у какого из них значение активации было максимальным. И так далее.

Ведь мы же хотим узнать, какой цифре от 0 до 9 соответствует входное изображение. Вы можете задуматься над тем, зачем нам использовать десять нейронов. Четырёх нейронов будет достаточно, поскольку 24=16, больше, чем 10 возможных значений. Естественно было бы использовать всего 4 выходных нейрона, каждый из которых принимал бы двоичное значение в зависимости от того, ближе его выходное значение к 0 или 1. Это ведь неэффективно? Зачем нашей сети использовать 10 нейронов? Однако остаётся вопрос, почему же 10 выходных нейронов лучше. Основание для этого эмпирическое; мы можем попробовать оба варианта сети, и окажется, что для данной задачи сеть с 10-ю выходными нейронами лучше обучается распознавать цифры, чем сеть с 4-мя. Есть ли какая-то эвристика, которая заранее сказала бы нам, что следует использовать 10 выходных нейронов вместо 4?

Рассмотрим сначала вариант с 10 выходными нейронами. Чтобы понять, почему, полезно подумать о том, что делает нейросеть. Он делает это, взвешивая свидетельства, полученные из скрытого слоя. Сконцентрируемся на первом выходном нейроне, который пытается решить, является ли входящее изображение нулём. Допустим, первый нейрон в скрытом слое определяет, есть ли на картинке что-то вроде такого: А что делают скрытые нейроны?

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

Как вы могли догадаться, все вместе эти четыре фрагмента дают изображение 0, которое мы видели ранее:

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

Если бы у нас было 4 выходных нейрона, тогда первый нейрон пытался бы решить, каков самый старший бит у входящей цифры. Если предположить, что сеть работает так, можно дать правдоподобное объяснение тому, почему лучше использовать 10 выходных нейронов вместо 4. Сложно представить какие-то исторические причины, по которым части формы цифры были бы как-то связаны с самым старшим битом выходных данных. И нет простого способа связать самый старший бит с простыми формами, приведёнными выше.

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

Упражнения

  • Существует способ определить побитовое представление числа, добавив дополнительный слой к трёхслойной сети. Дополнительный слой преобразует выходные значения предыдущего слоя в двоичный формат, как показано на рисунке ниже. Найдите наборы весов и смещений для нового выходного слоя. Предполагаем, что первые 3 слоя нейронов таковы, что правильный выход с третьего слоя (бывший выходной слой) активируется значениями не менее 0,99, а неправильные выходные значения не превышают 0,01.

Обучение с градиентным спуском

Итак, у нас есть схема НС – как ей обучиться распознавать цифры? Первое, что нам понадобится – это обучающие данные, т.н. набор обучающих данных. Мы будем использовать набор MNIST, содержащий десятки тысяч отсканированных изображений рукописных цифр, и их правильную классификацию. Название MNIST получил из-за того, что является изменённым подмножеством двух наборов данных, собранных NIST, Национальным институтом стандартов и технологий США. Вот несколько изображений из MNIST:

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

Первая содержит 60 000 изображений, предназначенных для обучения. Данные MNIST состоят из двух частей. Изображения чёрно-белые, размером 28х28 пикселей. Это отсканированные рукописные записи 250 человек, половина из которых были сотрудниками Бюро переписи населения США, а вторая половина – старшеклассниками. Это тоже чёрно-белые изображения 28х28 пикселей. Вторая часть набора данных MNIST – 10 000 изображений для проверки сети. Чтобы улучшить качество оценки, эти цифры были взяты у других 250 людей, не участвовавших в записи обучающего набора (хотя это тоже были сотрудники Бюро и старшеклассники). Мы будем использовать эти данные, чтобы оценить, насколько хорошо сеть научилась распознавать цифры. Это помогает нам убедиться в том, что наша система может распознавать рукописный ввод людей, который она не встречала при обучении.

Будет удобно относиться к каждому входному изображению х как к вектору с 28х28=784 измерениями. Обучающие входные данные мы обозначим через x. Выходное значение мы будем обозначать, как y=y(x), где у – десятимерный вектор. Каждая величина внутри вектора обозначает яркость одного пикселя изображения. T – операция транспонирования, превращающая вектор-строку в вектор-столбец. К примеру, если определённое обучающее изображение х содержит 6, тогда y(x)=(0,0,0,0,0,0,1,0,0,0)T будет нужным нам вектором.

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

$ C(w, b) = \frac{1}{2n} \sum_x || y(x) – a ||^2 \tag{6} $

Выход, конечно, зависит от x, w и b, но для простоты я не стал обозначать эту зависимость. Здесь w обозначает набор весов сети, b – набор смещений, n – количество обучающих входных данных, a – вектор выходных данных, когда х – входные данные, а сумма проходит по всем обучающим входным данным х. Мы будем называть C квадратичной функцией стоимости; иногда её ещё называют среднеквадратичной ошибкой, или MSE. Обозначение ||v|| означает длину вектора v. Кроме того, стоимость C(w,b) становится малой, то есть, C(w,b) ≈ 0, именно тогда, когда y(x) примерно равна выходному вектору a у всех обучающих входных данных х. Если присмотреться к C, видно, что она не отрицательна, поскольку все члены суммы неотрицательны. И наоборот, сработал плохо, когда C(w,b) большая – это означает, что y(x) не совпадает с выходом для большого количества входных данных. Так что наш алгоритм сработал хорошо, если сумел найти веса и смещения такие, что C(w,b) ≈ 0. Иначе говоря, нам нужно найти набор весов и смещений, минимизирующий значение стоимости. Получается, цель обучающего алгоритма – минимизация стоимости C(w,b) как функции весов и смещений. Мы будем делать это при помощи алгоритма под названием градиентный спуск.

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

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

Задача хорошо поставлена, однако пока что у неё есть много отвлекающих структур – интерпретация w и b как весов и смещений, притаившаяся на заднем плане функция σ, выбор архитектуры сети, MNIST и так далее. Подведём итог: наша цель в обучении НС сводится к поиску весов и смещений, минимизирующих квадратичную функцию стоимости C(w, b). Так что пока мы забудем об особой форме функции стоимости, связи с НС и так далее. Оказывается, что мы очень многое можем понять, игнорируя большую часть этой структуры, и концентрируясь только на аспекте минимизации. Мы разработаем технологию под названием градиентный спуск, которую можно использовать для решения таких задач. Вместо этого мы собираемся представить, что у нас просто есть функция со многими переменными, и мы хотим её минимизировать. А потом мы вернёмся к определённой функции, которую хотим минимизировать для НС.

Это может быть любая функция с вещественными значениями от многих переменных v = v1, v2,… Заметьте, что я заменил запись w и b на v, чтобы показать, что это может быть любая функция – мы уже не зацикливаемся на НС. Хорошо, допустим, мы пытаемся минимизировать некую функцию C(v). Полезно представлять себе, что у функции C есть только две переменных — v1 и v2:

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

Мы можем вычислить производные и попытаться использовать их для поиска экстремума. Один из способов решения задачи – использовать алгебру для поиска минимума аналитическим путём. Но при большом количестве переменных это превращается в кошмар. Если нам повезёт, это сработает, когда С будет функцией от одной-двух переменных. Использовать алгебру для минимизации этих функций не получится! А для НС нам часто нужно гораздо больше переменных – у крупнейших НС функции стоимости сложным образом зависят от миллиардов весов и смещений.

Прошу прощения. (Заявив, что нам будет удобнее считать C функцией двух переменных, я дважды в двух параграфах сказал «да, но что, если это функция гораздо большего количества переменных?». Просто иногда эта картинка разваливается, из-за чего и понадобились два предыдущих параграфа. Поверьте, что нам и правда будет полезно представлять С как функцию двух переменных. Для математических рассуждений часто требуется жонглировать несколькими интуитивными представлениями, обучаясь при этом тому, когда представление можно использовать, а когда нельзя.)

К счастью, существует прекрасная аналогия, предлагающая хорошо работающий алгоритм. Ладно, значит, алгебра не сработает. С последним графиком это будет не так сложно сделать. Мы представляем себе нашу функцию чем-то вроде долины. Наш опыт говорит нам, что шар в итоге скатится на самый низ. И мы представляем себе шар, катящийся по склону долины. Мы случайным образом выберем начальную точку для воображаемого шара, а потом симулируем движение шара, как будто он скатывается на дно долины. Возможно, мы можем использовать эту идею для поиска минимума функции? Эту симуляцию мы можем использовать просто подсчитывая производные (и, возможно, вторые производные) С – они скажут нам всё о локальной форме долины, и, следовательно, о том, как наш шарик будет катиться.

На самом деле, мы не будем так уж близко следовать этой аналогии с шаром – мы разрабатываем алгоритм минимизации С, а не точную симуляцию законов физики! На основе написанного вы можете подумать, что мы запишем уравнения движения Ньютона для шара, рассмотрим влияние трения и гравитации, и так далее. Так что вместо погружения в сложные детали физики, давайте зададим вопрос: если бы нас назначили богом на один день, и мы создавали бы свои законы физики, говоря шару, как ему катиться какой закон или законы движения мы бы выбрали, чтобы шар всегда скатывался на дно долины? Эта аналогия должна стимулировать наше воображение, а не ограничивать мышление.

Алгебра говорит нам, что С меняется следующим образом: Чтобы уточнить вопрос, подумаем, что произойдёт, если мы передвинем шар на небольшое расстояние Δv1 в направлении v1, и на небольшое расстояние Δv2 в направлении v2.

$ \Delta C \approx \frac{\partial C}{\partial v_1} \Delta v_1 + \frac{\partial C}{\partial v_2} \Delta v_2 \tag{7} $

Чтобы понять, как это сделать, полезно определить Δv как вектор изменений, то есть Δv ≡ (Δv1, Δv2) T, где Т – операция транспонирования, превращающая вектора-строки в вектора-столбцы. Мы наёдем способ выбора таких Δv1 и Δv2, чтобы ΔC была меньше нуля; то есть, мы будем выбирать их так, чтобы шар катился вниз. Обозначать градиентный вектор мы будем ∇С: Мы также определим градиент С как вектор частных производных (∂С/ ∂v1, ∂С/∂v2) T.

$ \nabla C \equiv (\frac{\partial C}{\partial v_1}, \frac{\partial C}{\partial v_2})^T \tag{8} $

А пока я хочу прояснить кое-что, из-за чего люди часто подвисают на градиенте. Скоро мы перепишем изменение ΔC через Δv и градиент ∇C. Что он конкретно означает? Впервые встретившись с записью ∇C, люди иногда не понимают, как они должны воспринимать символ ∇. С этой точки зрения, ∇ — это как размахивать флагом, сообщающим о том, что "∇С – это градиентный вектор". На самом деле можно спокойно считать ∇С единым математическим объектом – определённым ранее вектором – который просто записывается с использованием двух символов. Существуют и более продвинутые точки зрения, с которых ∇ можно рассматривать, как независимую математическую сущность (к примеру, как оператор дифференцирования), однако нам они не понадобятся.

С такими определениями выражение (7) можно переписать, как:

$ \Delta C \approx \nabla C \cdot \Delta v \tag{9} $

[англ. Это уравнение помогает объяснить, почему ∇C называют градиентным вектором: он связывает изменения в v с изменениями С, именно так, как и ожидается от сущности под названием градиент. перев.] Однако более интересно, что это уравнение позволяет нам увидеть, как выбрать Δv так, чтобы ΔC было отрицательным. gradient – отклонение / прим. Допустим, мы выберем

$ \Delta v = - \eta \nabla C \tag{10} $

Тогда уравнение (9) говорит нам о том, что ΔC ≈ — η ∇C ċ = — η ||∇C||2. где η — небольшой положительный параметр (скорость обучения). А именно это нам и надо! Поскольку ||∇C||2 ≥ 0, это гарантирует, что ΔC ≤ 0, то есть С будет всё время уменьшаться, если мы будем менять v, как прописано в (10) (конечно, в рамках аппроксимации из уравнения (9)). То есть, мы будем использовать уравнение (10) для вычисления значения Δv, а потом будем двигать шар на это значение: Поэтому мы возьмём уравнение (10) для определения «закона движения» шара в нашем алгоритме градиентного спуска.

$ v \rightarrow v' = v - \eta \nabla C \tag{11} $

Продолжая повторение, мы будем понижать С, пока, как мы надеемся, не достигнем глобального минимума. Потом мы снова применим это правило, для следующего хода.

Визуализировать это можно так: Подытоживая, градиентный спуск работает через последовательное вычисление градиента ∇ C, и последующее смещение в противоположном направлении, что приводит к «падению» по склону долины.

В реальной жизни у шара есть импульс, который может позволить ему катиться поперёк уклона, или даже какое-то время катиться вверх. Отметьте, что с этим правилом градиентный спуск не воспроизводит реального физического движения. Наше же правило выбора Δv просто говорит «иди вниз». Только после работы силы трения шар гарантированно скатится вниз долины. Достаточно неплохое правило для нахождения минимума!

В противном случае может получиться, что ΔC > 0 – ничего хорошего! Чтобы градиентный спуск работал правильно, нам нужно выбрать достаточно малое значение скорости обучения η, чтобы уравнение (9) было хорошей аппроксимацией. На практике η меняется так, чтобы уравнение (9) давало неплохую аппроксимацию, и при этом алгоритм работал не слишком медленно. В то же время, не нужно, чтобы η была слишком маленькой, поскольку тогда изменения Δv будут крохотными, и алгоритм будет работать слишком медленно. Позднее мы увидим, как это работает.

Но всё работает так же, если С будет функцией от многих переменных. Я объяснял градиентный спуск, когда функция С зависела только от двух переменных. Тогда изменение ΔC, вызванное небольшим изменением Δv = (Δv1,…, Δvm) T будет равняться Допустим, что у неё есть m переменных, v1,…, vm.

$ \Delta C \approx \nabla C \cdot \Delta v \tag{12} $

где градиент ∇C – это вектор

$ \nabla C \equiv (\frac{\partial C}{\partial v_1},…, \frac{\partial C}{\partial v_m})^T \tag{13} $

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

$ \Delta v = - \eta \nabla C \tag{14} $

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

$ v \rightarrow v' = v - \eta \nabla C \tag{15} $

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

Допустим, что мы пытаемся сдвинуться на Δv в позицию для максимального уменьшения С. В каком-то смысле градиентный спуск можно считать оптимальной стратегией поиска минимума. Мы ограничим размер шага так, чтобы ||Δv|| = ε для некоего малого постоянного ε > 0. Это эквивалентно минимизации ΔC ≈ ∇C ċ Δv. Можно доказать, что выбор Δv, минимизирующий ∇C ċ Δv, равняется Δv = -η∇C, где η = ε/||∇C||, определяется ограничением ||Δv|| = ε. Иначе говоря, мы хотим сдвинуться на небольшое расстояние фиксированного размера, и пытаемся найти направление движение, уменьшающее С так сильно, как это возможно. Так что градиентный спуск можно считать способом делать небольшие шаги в направлении, наиболее сильно уменьшающем С.

Упражнения

  • Докажите утверждение из последнего параграфа. Подсказка: если вы не знакомы с неравенством Коши — Буняковского, возможно, вам поможет, если вы узнаете о нём побольше.
  • Я объяснял про градиентный спуск, когда С является функцией двух переменных, и когда она является функцией многих переменных. Что будет, если С будет функцией всего одной переменной? Можете ли вы дать геометрическую интерпретацию работы градиентного спуска в одномерном случае?

Люди изучили множество вариантов градиентного спуска, включая и те, что более точно воспроизводят реальный физический шар. У таких вариантов есть свои преимущества, но и большой недостаток: необходимость вычисления вторых частных производных С, что может отнимать много ресурсов. Чтобы понять это, допустим, что нам надо вычислить все вторые частные производные ∂2C / ∂vj∂vk. Если переменных vj миллион, тогда нам нужно вычислить примерно триллион (миллион в квадрате) вторых частных производных (на самом деле, полтриллиона, поскольку ∂2C / ∂vj∂vk = ∂2C / ∂vk∂vj. Но суть вы уловили). Это потребует много вычислительных ресурсов. Есть трюки, помогающие избежать этого, и поиск альтернатив градиентного спуска является областью активных исследований. Однако в этой книге мы будем использовать градиентный спуск и его варианты в качестве основного подхода к обучению НС.

Нам нужно использовать его для поиска весов wk и смещений bl, минимизирующих уравнение стоимости (6). Как нам применить градиентный спуск к обучению НС? Иначе говоря, теперь у нашей «позиции» есть компоненты wk и bl, а у градиентного вектора ∇C есть соответствующие компоненты ∂C/∂wk и ∂C/∂bl. Давайте перезапишем правило обновления градиентного спуска, заменив переменные vj весами и смещениями. Записав наше правило обновления с новыми компонентами, мы получим:

$ w_k \rightarrow w'_k = w_k - \eta \frac{\partial C}{\partial w_k} \tag{16} $

$ b_l \rightarrow b'_l = b_l - \eta \frac{\partial C}{\partial b_l} \tag{17} $

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

Подробнее мы изучим их в следующих главах. Применению правила градиентного спуска есть несколько препятствий. Чтобы понять её, давайте вернёмся к квадратичной стоимости в уравнении (6). Но пока я хочу упомянуть только одну проблему. На практике для вычисления градиента ∇C нам нужно вычислять градиенты ∇Cx отдельно для каждого обучающего входа x, а потом усреднять их, ∇C = 1/n ∑x ∇Cx. Заметьте, что эта функция стоимости выглядит, как C = 1/n ∑x Cx, то есть это среднее по стоимости Cx ≡ (||y(x)−a||2)/2 для отдельных обучающих примеров. К сожалению, когда количество входных данных будет очень большим, это займёт очень много времени, и такое обучение будет проходить медленно.

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

Мы назовём эти случайные данные X1, X2,.., Xm, и назовём их мини-пакетом. Формулируя более точно, стохастический градиентный спуск работает через случайную выборку небольшого количества m обучающих входных данных. Если размер выборки m будет достаточно большим, среднее значение ∇C Xj будет достаточно близким к среднему по всем ∇Cx, то есть

$ \frac{\sum^m_{j=1} \nabla C_{X_j}}{m} \approx \frac{\sum_x \nabla C_x}{n} = \nabla C \tag{18} $

Поменяв части местами, мы получим где вторая сумма идёт по всему набору обучающих данных.

$ \nabla C \approx \frac{1}{m} \sum^m_{j=1} \nabla C_{X_j} \tag{19} $

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

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

$ w_k \rightarrow w'_k = w_k - \frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial w_k} \tag{20} $

$ b_l \rightarrow b'_l = b_l - \frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial b_l} \tag{21} $

Затем мы выбираем ещё один случайный мини-пакет и обучаемся на нём. где идёт суммирование по всем обучающим примерам Xj в текущем мини-пакете. В этот момент мы начинаем заново новую эпоху обучения. И так далее, пока мы не исчерпаем все обучающие данные, что называется окончанием обучающей эпохи.

В уравнении (6) мы масштабировали функцию стоимости в 1/n раз. Кстати, стоит отметить, что соглашения по поводу масштабирования функции стоимости и обновлений весов и смещений мини-пакетом разнятся. Это полезно, когда общее количество обучающих примеров заранее неизвестно. Иногда люди опускают 1/n, суммируя стоимости отдельных обучающих примеров, вместо вычисления среднего. Таким же образом правила обновления мини-пакета (20) и (21) иногда опускают член 1/m перед суммой. Такое может случиться, например, когда дополнительные данные появляются в реальном времени. Однако на это стоит обращать внимание при сравнении различных работ. Концептуально это ни на что не влияет, поскольку это эквивалентно изменению скорости обучения η.

К примеру, если наш обучающий набор имеет размер n = 60 000, как MNIST, и мы сделаем выборку мини-пакета размером m = 10, то в 6000 раз ускорит оценку градиента! Стохастический градиентный спуск можно представлять себе, как политическое голосование: гораздо проще взять выборку в виде мини-пакета, чем применить градиентный спуск к полной выборке – точно так же, как опрос на выходе из участка провести проще, чем провести полноценные выборы. На практике стохастический градиентный спуск – распространённая и мощная техника обучения НС, и база большинства обучающих технологий, которые мы разработаем в рамках книги. Конечно, оценка не будет идеальной – в ней будет статистическая флуктуация – но ей и не надо быть идеальной: нам лишь надо двигаться в примерно том направлении, которое уменьшает С, а это значит, что нам не нужно точно вычислять градиент.

Упражнения

  • Экстремальная версия градиентного спуска использует размер мини-пакета равный 1. То есть, при входных данных x мы обновляем наши веса и смещения по правилам wk → w′ k = wk − η ∂Cx / ∂wk и bl → b′l = bl − η ∂Cx / ∂bl. Затем мы выбираем другой пример обучающих входных данных и снова обновляем веса и смещения. И так далее. Эта процедура известна, как онлайн-, или инкрементальное обучение. В онлайн-обучении НС учиться на основе одного обучающего экземпляра входных данных за раз (как люди). Назовите одно преимущество и один недостаток онлайн-обучения по сравнению со стохастическим градиентным спуском с размером мини-пакета в 20.

Позвольте закончить этот раздел обсуждением темы, которая иногда беспокоит людей, впервые столкнувшихся с градиентным спуском. У НС стоимость С является функцией многих переменных – всех весов и смещений – и в каком-то смысле, определяет поверхность в очень многомерном пространстве. Люди начинают думать: «Мне придётся визуализировать все эти дополнительные измерения». И начинают волноваться: «Я не могу ориентироваться в четырёх измерениях, не говоря уже о пяти (или пяти миллионах)». Не отсутствует ли у них какое-то особое качество, имеющееся у «настоящих» сверхматематиков? Конечно же, нет. Даже профессиональные математики не могут визуализировать четырёхмерное пространство достаточно хорошо – если вообще могут. Они идут на хитрости, вырабатывая иные способы представления происходящего. Мы именно это и сделали: мы использовали алгебраическое (а не визуальное) представление ΔC, чтобы понять, как нужно двигаться, чтобы С уменьшалась. У людей, хорошо справляющихся с большим числом измерений, в уме находится большая библиотека подобных техник; наш алгебраический трюк – всего лишь один из примеров. Эти техники, возможно, не такие простые, к каким привыкли мы при визуализации трёх измерений, но когда вы создали библиотеку подобных техник, вы начинаете неплохо мыслить в высших измерениях. Не буду углубляться в детали, но если вам это интересно, вам может понравиться обсуждение некоторых из этих техник профессиональными математиками, привыкшими мыслить в высших измерениях. И хотя некоторые из обсуждаемых техник весьма сложны, большая часть лучших ответов интуитивны и доступны для всех.

Реализация сети для классификации цифр

Хорошо, теперь давайте напишем программу, обучающуюся распознаванию рукописных цифр с использованием стохастического градиентного спуска и обучающих данных от MNIST. Мы сделаем это при помощи короткой программы на python 2.7, состоящей всего из 74 строк! Первое, что нам нужно – скачать данные MNIST. Если вы используете git, тогда их можно получить, клонировав репозиторий этой книги:

git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git

Если нет, скачайте код по ссылке.

Таково официальное описание от MNIST. Кстати, когда раньше я упоминал данные MNIST, я сказал, что они разбиты на 60 000 обучающих изображений и 10 000 проверочных. Мы оставим проверочные изображения без изменений, но разобьём обучающий набор на две части: 50 000 картинок, которые мы будем использовать для обучения НС, и отдельные 10 000 картинок для дополнительной ратификации. Мы же разобьём данные немного по-другому. Хотя ратификационные данные не входят в изначальную спецификацию MNIST, многие используют MNIST именно так, и в области НС использование ратификационных данных встречается часто. Пока мы их не будем использовать, но позже они будут полезны нам, когда мы будем разбираться в настройке некоторых гиперпараметров НС – скорости обучения, и т.д., — которые наш алгоритм не выбирает напрямую. Теперь, говоря об «обучающих данных MNIST», я буду иметь в виду наши 50 000 каритнок, а не изначальные 60 000.

Если её у вас нет, вы можете взять её по ссылке. Кроме данных MNIST нам для быстрых вычислений линейной алгебры также понадобится библиотека для python под названием Numpy.

Центральное место занимает класс Network, который мы используем для представления НС. Перед тем, как выдать вам всю программу, позвольте объяснить основные особенности кода для НС. Вот код инициализации объекта Network:

class Network(object): def __init__(self, sizes): self.num_layers = len(sizes) self.sizes = sizes self.biases = [np.random.randn(y, 1) for y in sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]

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

net = Network([2, 3, 1])

Смещения и веса в объекте Network инициализируются случайным образом с использованием функции np.random.randn из Numpy, которая генерирует распределение Гаусса с математическим ожиданием 0 и среднеквадратичным отклонением 1. Такая случайная инициализация даёт нашему алгоритму стохастического градиентного спуска отправную точку. В следующих главах мы найдём лучшие способы инициализации весов и смещений, но пока хватит и этого. Отметьте, что код инициализации Network предполагает, что первый слой нейронов будет входным, и не назначает им смещения, поскольку они используются только при подсчёте выходных данных.

К примеру, net.weights[1] – матрица Numpy, хранящая веса, соединяющие второй и третий слои нейронов (это не первый и второй слои, поскольку в python нумерация элементов массива идет с нуля). Также отметьте, что смещения и веса хранятся как массив матриц Numpy. Это такая матрица, что wjk — это вес связи между k-м нейроном во втором слое и j-м нейроном в третьем. Поскольку писать net.weights[1] будет слишком долго, обозначим эту матрицу, как w. Но большим преимуществом такой записи будет то, что вектор активаций третьего слоя нейронов получается: Такой порядок индексов j и k может показаться странным — не было бы логичнее поменять их местами?

$ a' = \sigma (wa + b) \tag{22} $

a – вектор активаций второго слоя нейронов. Давайте разберём это довольно насыщенное уравнение. Затем мы применяем сигмоиду σ поэлементно к каждому элементу вектора wa+b (это называется векторизацией функции σ). Чтобы получить a', мы умножаем a на матрицу весов w, и добавляем вектор смещений b. Легко проверить, что уравнение (22) даёт такой же результат, что и правило (4) для вычисления сигмоидного нейрона.

Упражнение

  • Запишите уравнение (22) в компонентной форме, и убедитесь, что оно даёт тот же результат, что и правило (4) для вычисления сигмоидного нейрона.

Учитывая всё это, легко написать код, вычисляющий выходные данные объекта Network. Мы начнём с определения сигмоиды:

def sigmoid(z): return 1.0/(1.0+np.exp(-z))

Учтите, что когда параметр z будет вектором или массивом Numpy, то Numpy автоматически будет применять сигмоиду поэлементно, то есть, в векторном виде.

Предполагается, что параметр a – это (n, 1) Numpy ndarray, а не вектор (n,). Добавим метод прямого распространения в класс Network, который принимает на вход a от сети и возвращает соответствующие выходные данные. Если вы попытаетесь использовать вектор (n,), то получите странные результаты. Здесь n – количество входных нейронов.

Метод просто применяет уравнение (22) к каждому слою:

def feedforward(self, a): """Вернуть выходные данные сети при входных данных "a"""" for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a

Конечно, в основном нам от объектов Network нужно, чтобы они обучались. Для этого мы дадим им метод SGD, реализующий стохастический градиентный спуск. Вот его код. В некоторых местах он довольно загадочный, но ниже мы разберём его подробнее.

def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None): """Обучаем сеть при помощи мини-пакетов и стохастического градиентного спуска. training_data – список кортежей "(x, y)", обозначающих обучающие входные данные и желаемые выходные. Остальные обязательные параметры говорят сами за себя. Если test_data задан, тогда сеть будет оцениваться относительно проверочных данных после каждой эпохи, и будет выводиться текущий прогресс. Это полезно для отслеживания прогресса, однако существенно замедляет работу. """ if test_data: n_test = len(test_data) n = len(training_data) for j in xrange(epochs): random.shuffle(training_data) mini_batches = [ training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch(mini_batch, eta) if test_data: print "Epoch {0}: {1} / {2}".format( j, self.evaluate(test_data), n_test) else: print "Epoch {0} complete".format(j)

training_data – список кортежей "(x, y)", обозначающих обучающие входные данные и желаемые выходные. Переменные epochs и mini_batch_size – это количество эпох для обучения и размер мини-пакетов для использования. eta – скорость обучения, η. Если test_data задан, тогда сеть будет оцениваться относительно проверочных данных после каждой эпохи, и будет выводиться текущий прогресс. Это полезно для отслеживания прогресса, однако существенно замедляет работу.

В каждую эпоху он начинает с того, что случайно перемешивает обучающие данные, а потом разбивает их на мини-пакеты нужного размера. Код работает так. Затем для каждого mini_batch мы применяем один шаг градиентного спуска. Это простой способ создания выборки из обучающих данных. Вот код для метода update_mini_batch: Это делает код self.update_mini_batch(mini_batch, eta), обновляющий веса и смещения сети в соответствии с одной итерацией градиентного спуска, используя только обучающие данные в mini_batch.

def update_mini_batch(self, mini_batch, eta): """Обновить веса и смещения сети, применяя градиентный спуск с использованием обратного распространения к одному мини-пакету. mini_batch – это список кортежей (x, y), а eta – скорость обучения.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]

Большую часть работы делает строчка

delta_nabla_b, delta_nabla_w = self.backprop(x, y)

Она вызывает алгоритм обратного распространения – это быстрый способ вычислить градиент функции стоимости. Так что update_mini_batch просто вычисляет эти градиенты для каждого обучающего примера из mini_batch, а потом обновляет self.weights и self.biases.

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

За исключением функции self.backprop программа говорит сама за себя – основную работу проделывают self. Давайте посмотрим на программу целиком, включая поясняющие комментарии. Метод self.backprop использует несколько дополнительных функций для вычисления градиента, а именно, sigmoid_prime, вычисляющую производную сигмоиды, и self.cost_derivative, которую я не буду описывать здесь. SGD и self.update_mini_batch. В следующей главе мы рассмотрим их подробнее. Вы можете получить о них представление, изучив код и комментарии. На самом деле сама программа состоит всего из 74 не строк кода – не пустых и не комментариев. Учтите, что, хотя программа кажется длинной, большая часть кода – это комментарии, облегчающие понимание. Весь код доступен на GitHub.

"""
network.py
~~~~~~~~~~ Модуль реализации обучающего алгоритма стохастического градиентного спуска для нейросети прямого распространения. Градиенты вычисляются при помощи обратного распространения. Я специально делал код простым, читаемым и легко модифицируемым. Он не оптимизирован, и в нём нет многих желательных вещей. """ #### Библиотеки
# Стандартная библиотека
import random # Сторонние библиотеки import numpy as np class Network(object): def __init__(self, sizes): """ Массив sizes содержит количество нейронов в соответствующих слоях. Так что, если мы хотим создать объект Network с двумя нейронами в первом слое, тремя нейронами во втором слое, и одним нейроном в третьем, то мы запишем это, как [2, 3, 1]. Смещения и веса сети инициализируются случайным образом с использованием распределения Гаусса с математическим ожиданием 0 и среднеквадратичным отклонением 1. Предполагается, что первый слой нейронов будет входным, и поэтому у его нейронов нет смещений, поскольку они используются только при подсчёте выходных данных. """ self.num_layers = len(sizes) self.sizes = sizes self.biases = [np.random.randn(y, 1) for y in sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])] def feedforward(self, a): """Возвращает выходные данные сети, когда ``a`` - входные данные.""" for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None): """Обучаем сеть при помощи мини-пакетов и стохастического градиентного спуска. training_data – список кортежей "(x, y)", обозначающих обучающие входные данные и желаемые выходные. Остальные обязательные параметры говорят сами за себя. Если test_data задан, тогда сеть будет оцениваться относительно проверочных данных после каждой эпохи, и будет выводиться текущий прогресс. Это полезно для отслеживания прогресса, однако существенно замедляет работу. """ if test_data: n_test = len(test_data) n = len(training_data) for j in xrange(epochs): random.shuffle(training_data) mini_batches = [ training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch(mini_batch, eta) if test_data: print "Epoch {0}: {1} / {2}".format( j, self.evaluate(test_data), n_test) else: print "Epoch {0} complete".format(j) def update_mini_batch(self, mini_batch, eta): """Обновить веса и смещения сети, применяя градиентный спуск с использованием обратного распространения к одному мини-пакету. mini_batch – это список кортежей (x, y), а eta – скорость обучения.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """Вернуть кортеж ``(nabla_b, nabla_w)``, представляющий градиент для функции стоимости C_x. ``nabla_b`` и ``nabla_w`` - послойные списки массивов numpy, похожие на ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # прямой проход activation = x activations = [x] # список для послойного хранения активаций zs = [] # список для послойного хранения z-векторов for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # обратный проход delta = self.cost_derivative(activations[-1], y) * \ sigmoid_prime(zs[-1]) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) """Переменная l в цикле ниже используется не так, как описано во второй главе книги. l = 1 означает последний слой нейронов, l = 2 – предпоследний, и так далее. Мы пользуемся преимуществом того, что в python можно использовать отрицательные индексы в массивах.""" for l in xrange(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w) def evaluate(self, test_data): """Вернуть количество проверочных входных данных, для которых нейросеть выдаёт правильный результат. Выходные данные сети – это номер нейрона в последнем слое с наивысшим уровнем активации.""" test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data] return sum(int(x == y) for (x, y) in test_results) def cost_derivative(self, output_activations, y): """Вернуть вектор частных производных (чп C_x / чп a) для выходных активаций.""" return (output_activations-y) #### Разные функции
def sigmoid(z): """Сигмоида.""" return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): """Производная сигмоиды.""" return sigmoid(z)*(1-sigmoid(z))

Насколько хорошо программа распознаёт рукописные цифры? Начнём с загрузки данных MNIST. Сделаем это при помощи небольшой вспомогательной программы mnist_loader.py, которую я опишу ниже. Выполним следующие команды в оболочке python:

>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper()

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

Это мы сделаем после импорта описанной выше программы, которая называется network: После загрузки данных MNIST настроим сеть из 30 скрытых нейронов.

>>> import network
>>> net = network.Network([784, 30, 10])

Наконец, используем стохастический градиентный спуск для обучения на обучающих данных в течение 30 эпох, с размером мини-пакета в 10, и скоростью обучения η=3.0:

>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

Если вы выполняете код параллельно с чтением книги, учтите, что на его выполнение уйдёт несколько минут. Предлагаю вам всё запустить, продолжить чтение, и периодически проверять, что выдаёт программа. Если вы торопитесь, то можете уменьшить количество эпох, уменьшив количество скрытых нейронов, или используя только часть обучающих данных. Окончательный рабочий код будет работать быстрее: данные скрипты на python предназначены для того, чтобы вы поняли, как работает сеть, и не являются высокопроизводительными! И, конечно, после тренировки сеть может работать очень быстро почти на любой вычислительной платформе. К примеру, когда мы обучим сеть хорошему нбаору весов и смещений, её легко можно портировать для работы на JavaScript в веб-браузере, или в качестве нативного приложения на мобильном устройстве. В любом случае, примерно такой вывод даёт программа, обучающая нейросеть. Она пишет количество правильно распознанных проверочных изображений после каждой эпохи тренировок. Как видите, даже после одной эпохи она достигает точности в 9 129 из 10 000, и это число продолжает расти:

Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000

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

Как и раньше, если вы запускаете код одновременно с чтением, учтите, что его выполнение занимает довольно много времени (на моей машине каждая эпоха занимает несколько десятков секунд), поэтому лучше читать параллельно с исполнением кода. Давайте-ка перезапустим эксперимент, изменив количество скрытых нейронов до 100.

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

Естественно, это улучшает результат до 96,59%. По крайней мере, в этом случае использование большего количества скрытых нейронов помогает получать лучшие результаты.

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

Как я упоминал выше, их называют гиперпараметрами нашей НС – чтобы отличать их от простых параметров (весов и смещений), которые алгоритм настраивает в процессе обучения. Конечно, чтобы достичь такой точности, мне нужно было выбирать определённое количество эпох для обучения, размер мини-пакета и скорость обучения η. Допустим, к примеру, что мы выбрали скорость обучения η = 0,001: Если мы плохо выберем гиперпараметры, мы получим плохие результаты.

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)

Результаты получатся куда как менее впечатляющими:

Epoch 0: 1139 / 10000
Epoch 1: 1136 / 10000
Epoch 2: 1135 / 10000
...
Epoch 27: 2101 / 10000
Epoch 28: 2123 / 10000
Epoch 29: 2142 / 10000

Это говорит о том, что можно попробовать увеличить скорость обучения, допустим, до 0,01. Однако можно видеть, что эффективность сети со временем медленно растёт. Если сделать это несколько раз, мы в итоге придём к η = 1,0 (а иногда даже и 3,0), что близко к нашим более ранним экспериментам. В этом случае результаты будут лучше, что говорит о необходимости ещё больше увеличить скорость (если изменение улучшает ситуацию, изменяйте дальше!). Так что, хотя мы изначально плохо выбрали гиперпараметры, по крайней мере, мы собрали достаточно информации, чтобы суметь улучшить наш выбор параметров.

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

>>> net = network.Network([784, 30, 10])
>>> net.SGD(training_data, 30, 10, 100.0, test_data=test_data)

В итоге окажется, что мы зашли слишком далеко, и взяли слишком большую скорость:

Epoch 0: 1009 / 10000
Epoch 1: 1009 / 10000
Epoch 2: 1009 / 10000
Epoch 3: 1009 / 10000
...
Epoch 27: 982 / 10000
Epoch 28: 982 / 10000
Epoch 29: 982 / 10000

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

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

Упражнение

  • Попробуйте создать сеть всего из двух слоёв – входного и выходного, без скрытого – с 784 и 10 нейронами соответственно. Обучите сеть при помощи стохастического градиентного спуска. Какую точность классификации вы получите?

Ранее я пропустил детали загрузки данных MNIST. Она происходит довольно просто. Вот код для полноты картины. Структуры данных описаны в комментариях – всё просто, кортежи и массивы объектов Numpy ndarray (если вам не знакомы такие объекты, представляйте их себе как вектора).

"""
mnist_loader
~~~~~~~~~~~~ Библиотека загрузки изображений из базы MNIST. Детали структур описаны в комментариях к ``load_data`` и ``load_data_wrapper``. На практике, ``load_data_wrapper`` - это функция, которую обычно вызывает код НС. """ #### Библиотеки
# Стандартные
import cPickle
import gzip # Сторонние import numpy as np def load_data(): """Вернуть данные MNIST в виде кортежа, содержащего обучающие, ратификационные и проверочные данные. ``training_data`` возвращается как кортеж с двумя вхождениями. Первое содержит сами картинки. Это numpy ndarray с 50 000 элементами. Каждый элемент – это в свою очередь numpy ndarray с 784 значениями, представляющими 28 * 28 = 784 пикселя одного изображения MNIST.
Второе – это numpy ndarray с 50 000 элементами. Эти элементы – цифры от 0 до 9 для соответствующих изображений, содержащихся в первом вхождении.
``validation_data`` и ``test_data`` похожи, только содержат по 10 000 изображений.
Это удобный формат данных, но для использования в НС полезно будет немного изменить формат ``training_data``. Это делается в функции-обёртке ``load_data_wrapper()``. """ f = gzip.open('../data/mnist.pkl.gz', 'rb') training_data, validation_data, test_data = cPickle.load(f) f.close() return (training_data, validation_data, test_data) def load_data_wrapper(): """Вернуть кортеж, содержащий ``(training_data, validation_data, test_data)``. На основе ``load_data``, но формат удобнее для использования в нашей реализации НС.
В частности, ``training_data`` - это список из 50 000 кортежей из двух переменных, ``(x, y)``. ``x`` - это 784-размерный numpy.ndarray, содержащий входящее изображение. ``y`` - это 10-мерный numpy.ndarray, представляющий единичный вектор, соответствующий правильной цифре для ``x``.
``validation_data`` и ``test_data`` - это списки, содержащие по 10 000 кортежей из двух переменных, ``(x, y)``. ``x`` - это 784-размерный numpy.ndarray, содержащий входящее изображение, а ``y`` - это соответствующая классификация, то есть, цифровые значения (целые числа), соответствующие ``x``.
Очевидно, это означает, что для тренировочных и ратификационных данных мы используем немного разные форматы. Они оказываются наиболее удобными для использования в коде НС.""" tr_d, va_d, te_d = load_data() training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]] training_results = [vectorized_result(y) for y in tr_d[1]] training_data = zip(training_inputs, training_results) validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]] validation_data = zip(validation_inputs, va_d[1]) test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]] test_data = zip(test_inputs, te_d[1]) return (training_data, validation_data, test_data) def vectorized_result(j): """Вернуть 10-мерный единичный вектор с 1.0 в позиции j и нулями на остальных позициях. Это используется для преобразования цифры (0..9) в соответствующие выходные данные НС.""" e = np.zeros((10, 1)) e[j] = 1.0 return e

Я говорил, что наша программа достигает довольно неплохих результатов. Что это означает? Неплохих по сравнению с чем? Полезно бывает иметь результаты неких простых, базовых испытаний, с которыми можно было бы провести сравнение, чтобы понять, что значит «хорошие результаты». Простейшим базовым уровнем, конечно, будет случайная догадка. Это можно делать примерно в 10% случаев. А мы показываем гораздо лучший результат!

Давайте посмотрим на то, насколько темна картинка. Что насчёт менее тривиального базового уровня? К примеру, изображение 2 обычно будет темнее изображения 1, просто потому, что у неё больше тёмных пикселей, как видно на примерах ниже:

Получая новое изображение, мы вычисляем его темноту, и догадываемся, что на нём изображена цифра с ближайшей средней темнотой. Из этого следует, что мы можем вычислить среднюю темноту для каждой цифры от 0 до 9. Но это уже серьёзное улучшение по сравнению со случайными догадками – код правильно распознаёт 2 225 из 10 000 изображений, то есть, даёт точность 22,25%. Это простая процедура, которую легко запрограммировать, поэтому код я писать не буду – если интересно, он лежит на GitHub.

Поработав ещё немного, можно превысить 50%. Нетрудно разыскать и другие идеи, достигающие точности в диапазоне от 20 до 50%. Давайте попробуем один из наиболее известных алгоритмов, метод опорных векторов или SVM. Но чтобы достичь гораздо большей точности, лучше использовать авторитетные алгоритмы МО. Мы просто используем библиотеку python под названием scikit-learn, обеспечивающую простой интерфейс к быстрой библиотеке на C для SVM, известную, как LIBSVM. Если вам незнакомы SVM, не волнуйтесь, нам не потребуется разбираться в этих деталях.

Это уже большое улучшение по сравнению с наивным подходом классификации изображений по темноте. Если мы запустим SVM классификатор scikit-learn на настройках по умолчанию, то получим правильную классификацию 9 435 из 10 000 (код доступен по ссылке). В следующих главах мы познакомимся с новыми техниками, которые позволят нам улучшить наши НС настолько, что они сильно обгонят SVM. Это значит, что SVM работает примерно так же хорошо, как наша НС, только чуток похуже.

Результат 9 435 из 10 000 от scikit-learn указан для настроек по умолчанию. Но и это ещё не всё. Не буду углубляться в детали, их можно прочесть в статье Андреаса Мюллера. У SVM есть много параметров, допускающих подстройку, и можно искать параметры, улучшающие этот результат. Иначе говоря, хорошо настроенная SVM ошибается лишь в одной цифре из 70. Он показал, что проделав работу по оптимизации параметров, можно добиться точности не менее 98,5%. Могут ли НС достичь большего? Неплохой результат!

На сегодня хорошо настроенная НС обгоняет любую другую известную технологию в решении MNIST, включая SVM. Оказывается, могут. И большую часть использованных для этого технологий мы увидим в этой книге. Рекорд на 2013 год правильно классифицировал 9 979 из 10 000 изображений. Такой уровень точности близок к человеческому, а, возможно, и превышает его, поскольку несколько изображений из MNIST трудно расшифровать даже человеку, к примеру:

С такими изображениями в наборе данных MNIST удивительно, что НС может правильно распознать все изображения из 10 000, кроме 21. Думаю, вы согласитесь, что их сложно классифицировать! Но даже НС в работе-рекордсмене используют довольно простые алгоритмы, являющиеся небольшими вариациями тех, что мы рассмотрели в этой главе. Обычно программисты считают, что для решения подобной сложной задачи требуется сложный алгоритм. В каком-то смысле, мораль наших результатов и тех, что содержатся в более сложных работах, в том, что для некоторых задач
Вся сложность автоматически появляется во время обучения на основе обучающих данных.

сложный алгоритм ≤ простой алгоритм обучения + хорошие обучающие данные

К глубокому обучению

Хотя наша сеть демонстрирует впечатляющую эффективность, достигается она загадочным способом. Веса и смешения сети автоматически обнаруживаются. Значит, у нас нет готового объяснения того, как сеть делает то, что делает. Можно ли найти какой-то способ понять базовые принципы классификации сетью рукописных цифр? И можно ли, учитывая их, улучшить результат?

Поймём ли мы, как этот ИИ работает? Переформулируем эти вопросы более жёстко: допустим, что через несколько десятилетий НС превратятся в искусственный интеллект (ИИ). В ранние годы исследований ИИ люди надеялись, что попытки создания ИИ также помогут нам понять принципы, лежащие в основе интеллекта, и, может быть, даже работы человеческого мозга. Возможно, сети останутся непонятными для нас, с их весами и смещениями, поскольку они назначаются автоматически. Однако в итоге может оказаться, что мы не будем понимать ни мозг, ни то, как работает ИИ!

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

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

Мы попробуем вручную создать сеть, выбирая подходящие веса и смещения. Допустим, мы сделаем это, но не используя алгоритм обучения. Забыв на минуту о НС, мы могли бы разбить задачу на подзадачи: есть ли у изображения глаз в левом верхнем углу? Как мы можем к этому подступиться? Есть ли посередине нос? Есть ли глаз в правом верхнем углу? Есть ли вверху волосы? Есть ли внизу посередине рот? И так далее.

И наоборот, если ответы отрицательные, то лица, вероятно, нет. Если ответы на несколько из этих вопросов положительные, или даже «вероятно, да», тогда мы заключаем, что на изображении, возможно, есть лицо.

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

Есть ли глаз в правом верхнем углу?
В прямоугольниках – подзадачи: есть ли у изображения глаз в левом верхнем углу? Есть ли внизу посередине рот? Есть ли посередине нос? И так далее. Есть ли вверху волосы?

Возьмём вопрос с наличием глаза в левом верхнем углу. Возможно, что и подсети тоже можно разобрать на составляющие. Конечно, в вопросах должна содержаться информация о расположении – «Расположена ли бровь слева вверху, над зрачком?», и так далее – но давайте пока упростим. Его можно разобрать на вопросы типа: «Есть ли бровь?», «Есть ли ресницы?», «Есть ли зрачок?» и так далее. Поэтому сеть, отвечающую на вопрос о наличии глаза, можно разобрать на составляющие:


«Есть ли бровь?», «Есть ли ресницы?», «Есть ли зрачок?»

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

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

Определённо непрактично будет вручную подбирать веса и смещения. Конечно, я не говорил о том, как сделать это рекурсивное разбиение по подсетям. Исследователи в 1980-х и 1990-х пытались использовать стохастический градиентный спуск и обратное распространение для тренировки ГНС. Мы бы хотели использовать обучающие алгоритмы, чтобы сеть автоматически обучалась весам и смещениям – а через них и иерархии концепций – на основе обучающих данных. Сети обучались, но очень медленно, и на практике это было слишком медленно для того, чтобы это можно было как-то использовать. К сожалению, за исключением нескольких особых архитектур, у них ничего не получилось.

Они основаны на стохастическом градиентном спуске и обратном распространении, но содержат и новые идеи. С 2006 года было разработано несколько технологий, позволяющих обучать ГНС. И оказывается, что они гораздо лучше решают многие проблемы, чем неглубокие НС, то есть сети с одним скрытым слоем. Они позволили обучать гораздо более глубокие сети – сегодня люди спокойно обучают сети с 5-10 слоями. Это похоже на то, как языки программирования используют модульные схемы и идеи абстракций, чтобы на них можно было создавать сложные компьютерные программы. Причина, конечно, в том, что ГНС могут создавать сложную иерархию концепций. Абстракция в НС выглядит не так, как в языках программирования, но имеет такую же важность. Сравнивать глубокую НС с неглубокой НС – примерно как сравнивать язык программирования, умеющий делать вызовы функций, с языком, этого не умеющим.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть