К старту нашего флагманского курса по Data Science делимся расшифровкой видео от Себастьяна Лагу — разработчика игр, тьютора и популяризатора IT, который на своём
Начнём
Всем привет, я хочу попробовать научить свой компьютер распознавать разные каракули и просто картинки. Решить эту задачу можно разными способами, но сегодня меня интересуют нейросети.
Впервые это загадочное слово я услышал около 10 лет назад и вскоре попытался с помощью нейросети сделать так, чтобы маленькие существа из палочек ходили сами по себе. Мой код для обработки физики, видимо, был немного глючным, потому что это был мой самый успешный результат:
Не считая случаев, когда существо падало, а затем
В качестве второго эксперимента я решил попробовать
Конечно, поначалу это выглядит совершенно безнадёжно. Но, если мы позволим им конкурировать, а затем возьмём несколько лучших, клонируем несколько раз со случайными мутациями в их сетях, а затем позволим конкурировать уже им, и так далее, в итоге мы получаем автомобиль, который вполне успешно может ездить самостоятельно.
Последнее, что я пытался сделать пару лет назад, — обучить сети распознаванию цифр, написанных от руки. Как только мы создадим нашу маленькую нейронную сеть, это будет первой задачей, на которой мы её проверим:
После этого посмотрим, можно ли научить тот же код распознавать эти крошечные изображения предметов одежды и аксессуаров:
И, наконец, хотелось бы научить сеть распознавать рисунки десяти самых разных предметов: вертолётов, зонтиков, осьминогов и ветряных мельниц:
На самом деле, мы могли бы попробовать ещё
Но это значительный скачок в сложности. Поэтому, если для нашей простой сети это окажется слишком сложным, впоследствии нам придётся обновить её до
Границы принятия решений
Чтобы понять, как мы собираемся построить нашу нейронную сеть, давайте представим простой пример. Мы нашли необычный новый фрукт, пурпурный, колючий, с оранжевыми пятнами, очень вкусный:
Как ни странно, некоторые из этих фруктов оказались ядовитыми и вызывают ужасную боль в животе. Итак, давайте рассмотрим эти вымышленные фрукты. Мы увидим, что их внешний вид различается по двум основным параметрам: размер пятен и длина шипов:
Можно попробовать нарисовать график с размером пятен на одной оси и длиной шипов на другой. Затем мы могли бы собрать много разных фруктов и нанести их все на график на основе этих атрибутов.
Храбрые добровольцы согласились согласились пробовать эти фрукты, так мы смогли промаркировать, какие из них безопасны, а какие нет:
Если результат в конечном счёте выглядит вот таким вот случайным образом, то пятна и шипы, вероятно, не имеют отношения к тому, ядовит ли фрукт, и нам придётся придумать
Здесь можно провести небольшую линию, называемую границей принятия решения, и сказать, что любой фрукт, который находится по эту сторону границы, вероятно, ядовит, а по другую сторону, скорее всего, безвреден:
Итак, наш самый первый шаг — создать простую сеть, способную это определить.
Просто и понятно расскажем об IT и поможем стать востребованным профессионалом:
- «Уверенный старт в IT». Пройдите лучший курс для новичков: попробуйте 9 профессий и освойте подходящую именно вам.
- Полный курс по Data Science. Получите одну из самых перспективных профессий за 24 месяца.
- «C#-разработчик». Станьте профессионалом в разработке за 12 месяцев.
Веса
У нас есть два входных параметра для нашей задачи: размер пятен и длина шипов, а также два возможных результирующих параметра: безопасный или ядовитый. Если значение первого результирующего параметра больше второго, то мы предполагаем, что фрукт безопасен, если наоборот, то предполагается, что он ядовит.
Вы можете подумать, что два результирующих параметра — это довольно экстравагантно! Почему бы просто не иметь один? И сказать, что положительное значение означает безопасный, а отрицательное — ядовитый?
Мы, конечно, могли бы сделать так, но в следующих задачах будет более двух возможных результатов, поэтому полезно иметь отдельный результирующий параметр для каждого из них.
Итак, эти результирующие параметры, очевидно,
Таким образом, фактическое значение первого результирующего параметра будет равно первому входному параметру, умноженному на вес его соединения, и второму входному параметру, умноженному на вес его соединения. То же самое будет и со вторым результирующим параметром:
Я быстро написал код для выполнения этих расчётов. Вы можете увидеть их вот здесь, в функции Classify
:
Затем, чтобы визуализировать происходящее, есть функция Visualize
:
Она запускается для каждого пикселя на графическом дисплее и просит сеть предсказать, будет фрукт безопасным или ядовитым, а затем соответствующим образом окрашивает график. Итак, давайте посмотрим, что она делает. Сейчас результат выглядит очень параноидально. Кажется, нейросеть думает, что всё отравлено:
Но здесь, в углу, у нас есть веса нашей сети, так что я немного поиграю с ними. Посмотрим, смогу ли я получить ту границу принятия решения, которую мы хотим:
К сожалению, что бы я ни делал, я могу только заставить границу вращаться вокруг начала графика вот здесь, тогда как нам нужно иметь возможность сдвигать её по вертикали.
Смещения
Итак, давайте вернёмся к коду и немного модернизируем нашу сеть, добавив два новых значения под названием bias_1
и bias_2
. Они просто добавятся к взвешенным входным параметрам в коде, что позволит нам перемещать эти значения вверх или вниз:
Хорошо, давайте попробуем ещё раз. Я снова поиграю с весами, чтобы попытаться получить правильный угол границы, а затем перейду к смещениям.
Управлять этим немного сложно, но при помощи терпения мы в конце концов сможем вручную обучить небольшую сеть правильно классифицировать фрукты на нашей тренировочной выборке:
Это была довольно простая задача, поэтому давайте представим, что наши данные о фруктах на самом деле выглядели примерно так:
Это, конечно, сложнее, потому что мы больше не можем отделить безопасное от ядовитого с помощью прямой линии, поэтому нам нужно будет ещё немного обновить нашу нейронную сеть.
Скрытые слои
Один из способов улучшить нейронную сеть — просто увеличить её. Нет смысла менять количество входных или выходных параметров, потому что они определяются задачей, которую мы пытаемся решить. Вместо этого можно создать новый слой, который находится между ними. Эти «промежуточные» слои называются «скрытыми»:
Они могут иметь столько узлов, сколько нам нужно, и у нас может быть даже несколько скрытых слоёв, но давайте пока не будем усложнять.
Когда эти входные значения передаются на следующий слой, они будут умножаться на свои веса, как мы видели ранее, и суммироваться со значением смещения, вместе создавая взвешенные входные параметры для этого узла. Как только вычисления для всех узлов среднего слоя будут закончены, эти значения могут быть переданы дальше таким же образом, чтобы сформировать взвешенные входные параметры для следующего слоя:
Даже для этой крошечной сети, имеющей всего 12 весов и 5 смещений, довольно утомительно записывать все расчёты вручную, как я сделал здесь, поэтому я собираюсь выбросить этот код и поработать над более разумным решением.
Программируем нейросеть
Итак, я начал с создания скрипта Layer
, в котором хранятся значения веса для всех входящих соединений, а также значения смещения для каждого узла в слое. Здесь они настраиваются в зависимости от количества входящих и исходящих узлов:
Для ясности: в коде я представляю этот слой как имеющий, например, 3 входящих и 2 исходящих узла. Тогда в этом слое будет 2 входящих и 3 исходящих узла:
На самом же деле этот слой просто представляет значения, переданные нашей сети, он ничего не делает, поэтому он не будет настоящим слоем в коде.
Но вернёмся к скрипту Layer
. Всё, что осталось здесь рассмотреть, — функция CalculateOutputs
. Она принимает некоторые входные значения и вычисляет взвешенные входные данные, просто перебирая в цикле каждый исходящий узел, устанавливая соответствующий weightedInput
в значение смещения этого узла:
Затем она перебирает все входящие значения, умножая каждое из них на вес их соединения и добавляя результат к текущему weightedInput
. На данный момент эти взвешенные входные значения затем возвращаются в качестве результирующих значений слоя.
Далее у нас есть класс NeuralNetwork
, который содержит массив этих слоёв. Поэтому, когда создаётся сеть, ей нужно указать, сколько узлов должно быть в каждом слое, и она использует эту информацию, чтобы настроить их все так, как указано в коде ниже.
Теперь здесь находится функция для вычисления выходных значений всей сети, и всё, что она делает, — это перебирает все слои, вычисляет выходные значения каждого слоя и использует их в качестве входных значений для следующего слоя. Как только эти входные значения проходят через всю сеть, они становятся результирующими.
Наконец, функция Classify
работает практически так же, как и раньше, она просто вычисляет выходные значения и возвращает индекс наибольшего значения. Итак, сейчас я создам сеть с тремя слоями, что даст гораздо больше параметров для экспериментов, и давайте посмотрим, что ещё интересного мы можем сделать!
Что ж, оказывается, добавление дополнительного слоя совсем не помогает. По крайней мере само по себе. Чтобы сделать эту границу изогнутой, нам нужно позволить слоям оказывать нелинейное влияние на результат.
Функция активации
Итак, давайте вернёмся к нашему дизайну и увеличим масштаб одного узла.
В грубой аналогии с биологическими нейронными сетями мы можем представить этот узел как нейрон, а взвешенные входные значения будем считать своего рода стимулами. Если стимул достаточен, это должно вызвать срабатывание нейрона, что в нашей модели может означать вывод значения 1
; тогда как, если стимул меньше, чем нужно, нейрон не сработает, поэтому мы выводим 0
:
Я буду называть эти выходные значения «значениями активации». Нужно просто написать небольшую функцию, которая принимает взвешенные входные значения и вычисляет эти значения активации.
Давайте быстро покажем это на графике:
Таким образом, по оси X здесь показаны взвешенные входные значения, а по оси Y — соответствующие значения активации. Эта функция активации даёт нам более сложные границы принятия решений.
Однако, прежде чем мы сможем попробовать её, нужно перейти к скрипту Layer
. Давайте, вместо того чтобы выводить здесь взвешенные входные значения, пропустим их через функцию активации, а затем вместо них выведем эти значения активации:
Итак, возвращаясь к увлекательному процессу случайной настройки ползунков до тех пор, пока
И, конечно, увеличение размера сети сейчас позволит нам создавать всё более причудливые формы. Но этой крошечной сети уже достаточно, чтобы правильно сортировать воображаемые фрукты.
Кстати, вы могли заметить, что смещения больше не просто сдвигают всю границу вверх или вниз, вместо этого они легко смещают значение, которое входит в функцию активации. Итак, если вспомнить биологическую аналогию, это будет похоже на порог, который стимул должен превысить, чтобы активировать конкретный нейрон.
А теперь
Давайте вернёмся к нашему выбору функции активации и для решения проблемы заменим её
Теперь границы сглажены. Между прочим, эта функция называется сигмоидной. Сигмоидная функция — лишь одна из многих функций, с которыми люди экспериментировали для нейронных сетей. А вот ещё несколько, просто для интереса:
Давайте пока остановимся на этом и посмотрим, как изменится ситуация. Я ещё поиграю с этими ползунками — и у нас получатся красивые, плавные границы. И, что важнее, небольшая настройка одного ползунка больше не приводит к резкому изменению вывода:
Бесконечно пытаться обучать эту сеть вручную весело, но цель, конечно же, состоит в том, чтобы компьютер делал всё это сам. Нам нужен способ измерить, насколько хорошо это работает. Один из способов сделать это — просто подсчитать количество известных точек данных, которые классифицируются правильно. Но есть проблема: в большинстве случаев небольшая корректировка одного из ползунков на самом деле не изменит это число, ведь компьютер не знает, была ли корректировка полезной. Поэтому нужно попытаться найти способ измерять успехи поточнее.
Давайте снова подумаем о выходных значениях нашей сети, которые теперь скапливаются
Если мы даём сети входные данные о безопасном фрукте, то надеемся увидеть 1
в качестве первого выходного значения и 0
в качестве второго. Эти значения дают полную уверенность в безопасности фрукта. Для ядовитых фруктов всё должно быть наоборот.
Итак, мы знаем, какими должны быть выходные значения для наших обучающих данных, поэтому я добавил в скрипт Layer
небольшую функцию NodeCost
, которая принимает выходную активацию одного узла вместе с нужным значением:
Всё, что она делает, — вычисляет разницу между ними и возводит результат в квадрат, чтобы сделать его положительным и подчеркнуть, что большие различия требуют гораздо более срочного исправления, чем небольшие.
Затем я добавил в NeuralNetwork
функцию NodeCost
, которую ещё называют функцией потерь. Она принимает одну точку данных, например один из наших фруктов, и пропускает свои входные значения через сеть, чтобы получить выходные значения.
Затем, используя выходные значения, NodeCost перебирает все узлы в выходном слое и просто суммирует каждое из их отдельных значений, чтобы получить общее значение, — меру того, насколько хорошо работает сеть для заданной точки данных.
Однако нас больше всего интересует, как сеть работает со всеми точками данных, поэтому вот ещё одна версия функции потерь, которая на этот раз принимает несколько точек данных, просто суммирует результат для каждой из них и возвращает среднее значение.
Таким образом, вы можете видеть это значение в нижней части экрана, и цель сети теперь состоит в том, чтобы просто найти здесь значения для 17 весов и смещений, которые приводят к наименьшему среднему значению cost.
В случае нашей крошечной сети мы, вероятно, могли бы обойтись методом проб и ошибок, но сети будут становиться намного больше по мере решения всё более сложных задач, поэтому мы обязательно должны попытаться найти лучшее решение.
О нём поговорим в следующей статье цикла.
А ещё поможем освоить самые востребованные IT-профессии:
- «Уверенный старт в IT». Пройдите наш лучший курс для новичков: попробуйте 9 профессий и освойте подходящую именно вам.
- Полный курс по Data Science. Самая перспективная профессия за 24 месяца.
- «C#-разработчик». Станьте разработчиком с широким набором навыков за 12 месяцев.
Комментарии (21)
GospodinKolhoznik
20.09.2022 09:03+3Я так понимаю, человек сделал распозновалку рукописного алфавита - его программа может распознавать конечное число заранее определенных нарисованных литер [кошка, трактор, велосипед, ...] акуратно введенных в нужное поле. Такие задачи ещё в советские времена инженерами в НИИ на больших ламповых ЭВМ успешно решались, и даже без ЭВМ в целях удешевления - на конечных автоматах, собранных на рассыпухе. См. Бонгард М.М. "Проблема узнавания" Москва Наука 1967г.
Только инженеры НИИ не знали, что на этом можно хайпожорить и миллионы подписчиков собирать, поэтому жили на скромную зарплату в 120 рублей.
Alexey2005
20.09.2022 12:25+3Всё они прекрасно знали, просто считали ниже своего достоинства снисходить до какого-то быдла, которое всё равно ничего не понимает в науке.
Отношение советских учёных к пиарщикам и хайпожорам очень хорошо показано в произведении Стругацких «Понедельник начинается в субботу». Один из персонажей повести, профессор Выбегалло, за счёт пиара и хайпа сделал себе имя на крайне низкопробных исследованиях, уровня карикатурных «британских учёных». И что же? Хоть кто-нибудь взял его опыт на заметку? Хоть кому-то из этих магов пришла в голову идея распиарить и свои эксперименты тоже? Или хоть кому-то пришла в голову идея сделать этого Выбегалло главным пиарщиком всего НИИЧАВО, чтобы он писал научно-популярные статьи и книги, выпускал начно-популярный журнал о волшебстве и проблемах волшебной науки?
Разумеется нет — наоборот, они ещё и гордились тем, что настоящая наука скучна и непонятна простым смертным, и всячески презирали тех, кто злоупотребляет контактами с прессой.
Ну вот если сидеть в башне из слоновой кости, упиваясь собственным превосходством над «тупым быдлом», то обыватель рано или поздно задастся вопросом: а чем они там вообще занимаются? За что мы им вообще платим? Может, 120 руб — это даже много?
Refridgerator
20.09.2022 13:20+3Нейронные сети сейчас популярны потому, что там не нужно вникать в матчасть. «Аппроксимация», «свёртка», «корреляция», «обратная свёртка», «преобразование Фурье/Радона/etc» — эти слова я давно уже не видел в статьях о распознавании образов. Зато «миллионы изображений в датасете», «миллиарды параметров», «тысячи видеокарто-месяцев для обучения» — из каждого утюга.
thevlad
20.09.2022 14:49Почитайте сколько либо достойный учебник или научные статьи, путь нейросеток в computer vision, как раз начинался со сверточных нейронных сетей. Слова не видили в утюгах, потому что это либо и так очевидно тем кто читает, либо это расчитанный на хипстеров научпоп.
Refridgerator
20.09.2022 15:11Как я и говорил, в матчасть вникать никому не интересно. Суммирование значений нейронов помноженных на веса — это и есть свёртка, а слово «свёрточная» лишь определяет определенный класс нейросетей по архитектуре. Но лучше вам об этом расскажут авторы курса.
thevlad
20.09.2022 15:47Да уж, ясно. Свертка если совсем по простому это линейный фильтр, а умножение весов, это скалярное произведение(dot product) двух векторов. Понимаете в чем разница? Свертку можно реализовать через повторяющееся скалярное произведение, но скалярное произведение не является сверткой.
Refridgerator
20.09.2022 20:37Свёртка, если не по простому, а по правильному — это интегральное преобразование над двумя функциями, дающее в результате третью. В дискретном пространстве, соответственно, интеграл превращается в сумму, и если в искомой функции нас интересуют не все её значения, а только в одной точке — соответственно и все другие значения считать не обязательно.
Ну и насколько я помню, скалярное произведение для векторов разной размерности не определено.thevlad
20.09.2022 21:05Не ну вы серьезно хотите меня убедить, что скалярное произведение векторов надо называть сверткой выродившейся в одну точку?
Классическая нейросеть это как раз и есть последовательность скалярных произведений на которых действует нелинейность.
Ну и определениями меня поучать точно не стоит, для дискретных отсчетов линейный фильтр и свертка это одно и тоже. А так как речь зашла про нейросетки, то не дискретными они не могут быть в принципе.
Refridgerator
20.09.2022 21:45Я не хочу вас ни в чём убеждать. Математически распознавание по шаблону осуществляется через свёртку, через неё же всё строго обосновывается. Вырождение очевидно происходит потому, что идеальная корреляция даёт дельта-функцию Дирака. А вот каким образом получается распознавать образы через скалярное умножение — уже не так очевидно. Как оно вам поможет улучшить качество распознавания при недостатке обучающего материала неочевидно тоже.
thevlad
20.09.2022 23:23Теперь вы хотя бы ответили по существу, спасибо. Одним из методов template matching действительно является поиск максимума кросс-корреляции(которую можно представить как свертку с "отраженным" ядром) между шаблоном и изображением. Так же этот метод является наиболее простым и старым. Если мы будем рассматривать задачу из статьи, то template matching через корреляцию с шаблоном, будет не устойчив даже относительно тривиальных трансформаций сжатия/расширения относительно одной из осей. Не говоря уж об более сложных аффинных преобразованиях, а примеры в статье на самом деле еще сложнее.
>Вырождение очевидно происходит потому, что идеальная корреляция >даёт дельта-функцию Дирака.
Кросс-корреляция двух функций дает на выходе функцию. Скалярное произведение двух векторов дает скаляр. Кросс-корреляция с шаблоном очевидно никакую дельта-функцию не выдает, это будет функция "похожести" зависящая от координат шаблона, в точке где изображение и шаблон полностью перекрываются она будет равняться 1.
PS: поискал немного по теме, кросс-корреляция является оптимальной линейной оценкой относительно аддитивного белого гауссова шума, это единственная фундаментальная причина почему ее стоит использовать.
Refridgerator
21.09.2022 05:53Вот пример функции, автокорреляция которой даст единичный импульс:
Вариация ЛЧМТак получилось не случайно, а потому что её спектр константен по амплитуде. Для всех прочих сигналов нужно просто обратить амплитуду в спектре, чтобы получить схожий результат. Аргумент функции коллеляции — это время, и в единицу времени она также имеет единственное значение, если этот момент вас смущает.
thevlad
21.09.2022 11:10О госпади, естественно можно подобрать какието функции свертка которых даст дельта-функцию. Только к исходным вашим утверждениям, как и к задаче распознавания по шаблону, это вообще никакого отношения не имеет.
Refridgerator
21.09.2022 11:23Ну вот нейросеть и подбирает «какие-то функции» через процесс обучения. А без обучения «какие-то функции» не подбираются, а вычисляются.
thevlad
21.09.2022 11:31Нейросеть не реализуется сверткой, там вычисляемая функция совершенно другая, на несколько порядков сложнее и "подбирается" она вполне определенным методом - градиентным спуском по весам в пространстве функционала ошибки. Одиночная свертка применима только для тривиального поиска по шаблону. Передергивание с дельта-функцией вообще не понятно к чему было.
stranger777
20.09.2022 14:55+1«Аппроксимация», «свёртка», «корреляция», «обратная свёртка», «преобразование Фурье/Радона/etc» — эти слова я давно уже не видел
Учту это при отборе материала. В матчасть, конечно, вникать нужно — и нужно это как воздух. Иначе от слов Data Scientist останется одно Data. Поэтому у нас есть, как минимум, курс «Математика для Data Science» — это часть полного курса по Data Science. Цитирую:
«Чтобы уверенно решать не типовые задачи и создавать собственные архитектуры, мало владеть основными методами машинного обучения и нейронных сетей: важно понимать законы математики и статистики у них «под капотом».
По-хорошему, на мой взгляд, каждая модель от Data Scientist должна представлять собой некую малую (или крупную) научную работу в прикладной области, чтобы научными методами решить конкретную задачу или проблему.
TheShock
Кажется, эта нейросеть просто смотрит в будущее, что там будет нарисовано)
a-tk
Эта нейросеть всего лишь скоррелировала на 31.08% поданное изображение с изображением "какого-то трактора" в обучающей выборке. Видимо, кружок указанного размера заметно отличает этот образ в обучающей выборке от остальных изображений этой же обучающей выборки.
Siddthartha
то есть -- "смотрит в будущее".. )
a-tk
К сожалению, только в прошлое...