В сети полно примеров программного кода нейронных сетей, однако подавляющее большинство составлено с использованием дополнительных библиотек и «математических приёмчиков» — не вдаваясь при этом в какие‑либо подробности — мол, работает шайтан — и уже хорошо, главное — что «уместилось» в N строчек кода и «объяснено» всего за M минут.
Не берусь утверждать, что нейросети — это «элементарная база», но всё же попытаюсь максимально упростить эту тему; по возможности, не утрачивая информативность.
«Нейрон» — биологическое название нервной клетки; биологическая «нейронная сеть» — нервная система. Искусственная нейронная сеть — это реляционная база данных, в которой коэффициенты реляций — они же «веса связей», между данными в ячейках — они же «узлы» — заведомо неизвестны и генерируются псевдо‑случайным образом — этот процесс называется инициализацией нейросети. Такая нейросеть (инициализированная) со случайными коэффициентами «весов связей» на практике бесполезна и является «необученной». Чтобы «обучить» нейросеть выдавать более‑менее точные и, следовательно, полезные расчёты — необходимо дать ей заведомо точные результаты расчётов и программу «машинного обучения», которую в данном контексте можно считать искусственным интеллектом — хотя на деле, если докопаться до деталей — эта программа не намного «интеллектуальнее» пресловутого программного сортировщика или генератора псевдослучайных чисел.
Программа машинного обучения генерирует случайное число (инициализация) из указанного диапазона (обычно от -0.5 до +0.5) в качестве коэффициента — он же «вес«; затем входные данные умножаются на этот коэффициент, а все такие произведения для каждого отдельно взятого узла — суммируются (прямое распространение). Эта сумма произведений далее «причёсывается» так называемой «функцией активации«, чтобы умещаться в конечный диапазон (обычно от 0 до 1) — в качестве такой функции чаще всего используется «сигмоида» — подробнее о ней напишу чуть ниже. Вывод из функции активации сравнивается с требуемым результатом (функция потерь) — в зависимости от результата сравнения корректируется значение «веса» для следующей итерации (обратное распространение) — и эта рекурсия может повторяться до бесконечности, с каждым шагом потенциально сужая диапазон вероятных значений. То, на сколько этот диапазон уменьшился относительно начального диапазона — отражает текущий показатель точности прогнозов нейросети; например, если из диапазона «0 — 1» остался диапазон «0.87 — 0.88», то такая нейросеть достигла уверенности (точности прогнозов) — 99%.
Замена данных во входном слое обученной нейросети на новые данные (для прямого распространения) — позволяет классифицировать эти данные с помощью этой нейросети.
Обладая обученной нейросетью, выдающей достаточно точные прогнозы — можно пропускать через эту нейросеть (через эту реляционную базу данных) уже другие данные, которых не было в учебном датасете (dataset — коллекция данных) — где нам заведомо неизвестен правильный расчёт, но исходя из похожести новых данных на данные из обучающей выборки и исходя из точности подобранных на учебной выборке «параметров» мы можем обладать некоторой уверенностью в том, что рассчитанный такой нейросетью прогноз по своей точности практически не уступит точности прогнозов в учебном датасете.
Эта уверенность — равняется произведению двух коэффициентов: первый — это точность прогнозов нейросети на учебном датасете, и второй — это похожесть новых данных на данные из учебного сета. Например, если нейросеть обучалась на картинках кошек и собак и достигла 99% точности в опознании тех и других, но если новые данные для такой нейросети не будут похожи ни на собак ни на кошек, а будут даны, к примеру, бобёр и синица — то нейросеть, конечно же, скажет, кто из них больше похож на собаку, а кто на кошку, но не более того — такая нейросеть никогда не даст разумного ответа о данных, сильно отличающихся от всего на чём она училась.
По сути — выходит, что нейросети — это инструмент классификации и автоматизации прогнозов, применимый к относительно стабильным системам — это такие системы, которые либо практически не меняются со временем (меняются значительно медленнее, чем нейросеть обучается), либо меняются циклично — что позволяет создавать учебные датасеты в очень высокой степени похожие на реальные данные, с которыми в будущем будет работать нейросеть. Например — анатомию тела можно считать стабильной системой: она может не меняться тысячелетиями. Это позволяет калибровать нейросети на датасетах людей с различными недугами, обучая их ставить не менее точные диагнозы, чем самые квалифицированные медики. И обратный пример: спортивные матчи, фондовый рынок, траектории элементарных частиц — нестабильные, хаотичные системы, где предсказания даже самой прокаченной нейросети, как и любого человека-эксперта — могут нести только развлекательный характер. Не то что бы всё это было совсем бесполезно — нет; среднерыночную прибыль, сопоставимую с банковским вкладом, можно с высокой долей вероятности «поднять» на бирже, если строго придерживаться инвестиционных советов ботов — просто их показатели не какие-то выдающиеся, а очень даже средние, если не сказать минимальные — ведь в конечном счёте, финансовые боты позиционируются именно как инструмент сбора и анализа огромных объёмов данных, на основе которых они всего лишь вычисляют показатель риска и составляют чарты по наименее рискованным инвестициям — но не прогнозируют прибыль вкладов и, конечно же, не гарантируют её. Да — если сравнивать «игру вслепую» со стратегией искусственного интеллекта — само собой, стратегия будет (в среднем) более выгодна — но эта выгода будет минимальна — меньше чем, например, темпы роста биткоина — но и с меньшими рисками. Именно поэтому многие инвесторы были шокированы внезапной закупкой Илоном Маском криптовалюты на полтора миллиарда американских долларов, но что с того? Миллиардер не прогадал и заработал на этом; его акции поднялись больше, чем акции тех, кто придерживается стратегии BlackRock — получается, его рискованный прогноз оказался прибыльнее общего «расчётливого тренда» — и это лишь один пример из бесчисленного множества.
Прямое распространение — это преобразование входных данных (особенностей) в сигналы на выходе (классы или прогнозы). Прямое распространение применяет к данным (умножает) текущие (подобранные в процессе обучения) коэффициенты связей (параметры), суммирует такие вводы для каждого узла и пропускает полученный результат через фильтр — через функцию активации.
Прямое распространение влияет на значения узлов (нейронов).
Функция активации — по сути просто аппроксимация; применяется в тех случаях, где ответ должен укладываться в конечный диапазон — когда значения ведущих к ответу узлов, помноженных на коэффициенты связей, в этот диапазон не укладываются и вообще могут быть изначально никак не ограничены. Подобное «сжатие» диапазона ответов называется функцией активации, которые бывают разные, например «сигмоида» —
Примечание: тот же символ (сигма) — часто используется для формулы среднеквадратичного отклонения, которая очень похожа на среднеквадратичную ошибку, о которой пойдёт речь чуть ниже — но всё же, они разные: в среднеквадратичной ошибке нет операции извлечения квадратного корня.
Backward Propagation — обратное распространение (ошибки)
Каждый узел выходного слоя можно обусловить как какой-то произвольный класс — после чего итеративно (пошагово) модифицировать параметры нейросети таким образом, чтобы поступающие в неё данные, проходя через эту нейросеть, разрешались в один из этих классов — подобно тому как у людей различные визуальные образы ассоциируются с различными смыслами.
Если прямое распространение выглядит довольно прозрачно — умножение, сложение и аппроксимация, то обратное распространение — процесс вычисления «неверных» связей — может показаться чуть менее очевидным.
Обратное распространение — это процесс коррекции параметров; его же можно воспринимать как процесс обучения. Начинается с функции потерь и продолжается математическим инструментом «производная«, вытекающим в «цепное правило«.
Обратное распространение влияет на значения коэффициентов связей (весов).
Смысл «обратного распространения ошибки» — в том, чтобы максимизировать вывод на «правильных» узлах — соответствующих корректным классам, и минимизировать ошибку — выводы на всех остальных выходных узлах.
Первый шаг в обратном распространении — расчёт влияния так называемой «стоимости ошибки» (на математическом языке — функция потерь) из последнего слоя — слоя вывода, на коэффициенты связей (параметры), ведущие к этому слою.
На математическом языке, композитную (сложную) функцию от функции потерь для каждого выходного узла искусственной нейросети можно сформулировать следующим образом (если выглядит страшно, то, наверное, можно её пока пропустить):
Эта же формула, но записанная по-другому (я не издеваюсь, вдруг так правда проще?):
где z — сложная функция (прямого распространения) от всех параметров нейросети и функции потерь для выходного узла n; f — функция потерь; σ — функция активации (на примере сигмоиды); k — количество скрытых слоёв нейросети; i — количество узлов в слое; n — номер выходного класса.
Объединив все переменные в общую композитную функцию прямого распространения по нейросети — с помощью правил дифференцирования, можно определить индивидуальный вклад в функцию от каждой переменной — что, в свою очередь, необходимо для коррекции (обучения) параметров искусственной нейросети в ходе обратного распространения ошибки.
Но перед, непосредственно, «обратным распространением» — необходима небольшая ремарка об упомянутой выше функции потерь.
Существуют разные функции потерь — для разных ситуаций и для продвинутых пользователей, но в большинстве случаев можно обойтись функцией среднеквадратичной ошибки (Mean Squared Error): MSE возводит в квадрат разность полученного результата и ожидаемого (эталонного), затем суммирует все такие квадраты разностей между собой и делит на количество этих квадратов. MSE удобна своей универсальностью, поскольку не важно что из чего вычитать — умножение результата самого на себя (возведение в чётную степень) всегда будет давать положительный результат:
Вообще говоря — для обучения искусственной нейросети, сам показатель не важен — значение имеет лишь его динамика.
В математике уже давно изобретён (или открыт?) инструмент для исследования динамики функций — и называется он «производная». Произво́дная функции — понятие дифференциального исчисления, характеризующее скорость изменения функции в данной точке. Применимо к искусственным нейросетям это означает, что с помощью частной производной (и цепного правила) можно вычислить вклад каждой отдельно взятой нейронной связи в общую функцию потерь.
Объявим функцию потерь от двух переменных:
Тогда общая функция потерь будет выглядеть так:
где n — количество классов (выходных узлов) в нейросети.
Общая функция потерь на основе среднеквадратичной ошибки:
С помощью правил дифференцирования функции от нескольких переменных, находим частные производные (общей) функции потерь по прогнозам (они нужны для последующих вычислений), принимая все остальные слагаемые за постоянные (производная которых равна нулю):
Примечательно, что этот процесс дифференцирования делает равными между собой производные от общей функции потерь и от какой-либо отдельно взятой функции потерь по какой-то конкретной переменной:
К тому же, множитель (2/n) в данном случае, как и ноль, не играет никакой роли — всё равно дальше всё будет умножено на параметр «шага обучения», который задаётся произвольным образом.
В итоге — в контексте последнего слоя нейросети, частная производная функции потерь (общей или частной) по какому-то конкретному выводу выглядит так:
где i — номер (индекс) выходного узла (класса).
Эту формула напоминает игру «холодно-горячо», где она в буквальном смысле выдаёт значения температуры, только наоборот: при эталоне равном нулю, вычитание нуля из любого значения будет вытекать в положительные значения производной; а при эталоне равном единице — вычитание единицы из любого меньшего числа будет давать отрицательную производную функции потерь.
Положительную производную функции потерь по прогнозам — можно понимать как количество ошибки; а отрицательная производная функции потерь относительно прогнозов — указывает на степень правильности.
К сожалению, такая «обратная» логика не очень хорошо укладывается в общую картину нейросетей, где принято увеличивать значения «на верном пути» и уменьшать ошибку — поэтому полученный результат умножается на отрицательный коэффициент шага обучения — этот множитель определяет скорость обучения, задаётся вручную и его калибровка позволяет снизить количество эпох — итераций обучения, необходимых для достижения приемлемой уверенности в прогнозах.
Производная функции потерь по прогнозам, умноженная на (отрицательный) шаг обучения — это и есть самые первые параметры, рассчитанные в самом начале того, что уже называется «обратным распространением ошибки».
Цепное правило: производная композиции функций
В переводе на человеческий язык, формула выше означает, что производная сложной (композитной) функции равна произведению производных — внешней функции и внутренней.
Примечание: в обозначениях Лейбница цепное правило для вычисления производной функции z = z(f), где f = f(x), принимает следующий вид:
Для дальнейших определений, введём понятие «взвешиваемого узла» — это следующий за каким-то конкретным весом (параметром) узел (нейрон). При прямом распространении, значения узлов умножаются на «исходящие» из них параметры — так вот, вес в контексте «взвешиваемого узла» — это именно «входящий», а не «исходящий» вес.
Применяя цепное правило относительно коэффициентов связей — можно вычислить частную производную общей функции потерь по весу связи «w» — градиент по параметру «w»
Частная производная общей функции потерь по какому-то весу «w» — равна производной общей функции потерь по выводу взвешиваемого этим весом узла, умноженной на производную вывода (взвешиваемого узла) по вводу (взвешиваемого узла), умноженной на производную ввода (взвешиваемого узла) по этому весу:
примечание: цифры в квадратных скобках перед выражением — означают его «уровень»; я добавил их для наглядности — далее по тексту будет ясно, зачем они. Математического смысла они не несут, в вычислениях не участвуют!
Рассмотрим по отдельности каждый множитель из этой формулы; начнём с первого — производная общей функции потерь по выводу узла «х».
Поскольку общая функция потерь является суммой всех функций потерь,
то это же верно и для её производной (одно из правил):
Перепишем это же уравнение по отношению к выводу какого-то узла «х»:
где n — количество выходных классов; x — индекс взвешиваемого узла.
Каждое слагаемое из этой суммы можно найти по следующей формуле:
где i — целое число от 1 до n, где n — количество выходных узлов нейросети.
При прямом распространении, ввод узла «i» вычисляется простым умножением значения (вывода) узла «x» на коэффициент исходящей из него связи «w»(xi):
Следовательно — искомая в ходе обратного распространения производная ввода следующего слоя от вывода предыдущего — равна коэффициенту связи (весу) между взвешиваемым узлом и этим вводом:
Производная функции потерь по вводу — как сложная функция, раскладывается в произведение производных:
Ну а здесь уже всё «родное» — первый множитель вспомним из недавних расчётов:
По поводу второго множителя — поскольку вывод с какого-либо узла является функцией активации от его ввода,
то производная вывода по вводу — равняется производной от функции активации по этому вводу, применённой в ходе прямого распространения ко взвешиваемому узлу:
Подставим найденные значения в формулу «уровнем» выше:
И ещё выше — в итоге, получаем формулу производной частной функции потерь по выводу взвешиваемого узла:
В этой формуле примечательно использование веса w(xi), исходящего из взвешиваемого узла: именно поэтому данный метод коррекции называется «обратным распространением ошибки» — потому что первыми по порядку корректируются значения выходных параметров, а за ними — на их основе, корректируются более глубокие параметры — и так далее — с последнего слоя нейросети до первого.
Далее — подставляем всё это в формулу производной общей функции потерь по выводу какого-то узла «х»:
где n — количество выходных узлов нейросети; w — параметр, исходящий из взвешиваемого узла.
Вернёмся к «главной» формуле:
-
Первый множитель — только что обозначенная сумма;
-
Производная вывода узла по его вводу — это производная от его функции активации:
-
Производная от ввода по весу — просто равняется предыдущему выводу, из которого идёт связь с искомым весом:
где p — индекс узла из которого идёт связь со взвешиваемым узлом; w(px) — входящая связь во взвешиваемый узел. Смещение (bias) — «голый» вес без коэффициента узла — важный элемент, без которого нейросеть работает менее точно, чем с ним. Ну правда — я не знаю как лучше его объяснить, да и зачем? Иногда лучше просто «shut up and calculate».
Итоговый вид формулы производной общей функции потерь по весу «w» до узла «x»:
-
для узла «х» из последнего слоя:
-
для узла «х» из всех остальных слоёв:
где x — индекс взвешиваемого узла; p — индекс «исходящего» узла; n — количество выходных узлов в нейросети; х ∉ p ∉ n
Примечания:
-
если индекс p относится к «узлу смещения» (bias), то переменная «вывод(р)» упраздняется в единицу;
-
w(px) — вес связи от узла «p» до узла «x»;
-
w(xi) — вес связи от узла «х» до узла «i».
Итак, нейросеть — это некоторое множество «вводов», на каждый из которых поступают данные из разных источников (подобно нервным окончаниям у людей) и связи различной силы этих «вводов» с «выводами», которые эта нейросеть делает из полученных данных. Что-то ещё?
Ну, во-первых — прямая связь «ввода» с «выводом» — это то, что называется одним «слоем» нейронной сети, а таких слоёв может быть множество — где «вывод» одного слоя — это «ввод» следующего. Если у нейронной сети больше одного слоя, то все «промежуточные» слои — называются «скрытыми» (hidden). Когда у нейросети более одного скрытого слоя — то такая сеть считается «глубокой» — от термина «глубокое обучение» (deep learning), посвящённого, собственно, обучению таких сетей. Смысл таких слоёв (и «глубокого обучения») в том, чтобы усложнить связи между вводом и выводом — добавить коэффициенты к коэффициентам, позволяющие более гибко адаптировать их значения под требуемые результаты.
С увеличением количества скрытых слоев и количества узлов в этих слоях увеличивается «выразительность» сети — её способность выделять зависимости и закономерности во входных данных. Однако с увеличением сложности сети, каждый следующий слой поднимает затраты на обучение такой сети — в геометрической прогрессии; кроме того — слишком «сложная» нейросеть — с избытком скрытых слоёв — будет страдать от «переобучения» — это когда нейросеть слишком хорошо «запоминает» обучающий датасет и утрачивает способность обобщать его с новыми данными. Поэтому — выбор оптимального количества скрытых слоев и количества узлов в этих слоях является важной задачей при проектировании нейросети и требует тщательного анализа и экспериментов.
Во-вторых — так называемые «нейроны смещения» — узлы с фиксированным значением — добавляются к каждому скрытому слою и не имеют «входных» связей: своеобразные усилители сигнала.
В-третьих — «функция активации» — по сути просто аппроксимация; применяется в тех случаях, где ответ должен укладываться в конечный диапазон. Например — функция активации ReLU
Ну и последнее — «функция потерь» — это первый градиент, с которого начинается обратное распространение.
Cписок всех логических элементов искусственной нейронной сети:
-
входные узлы (учебный датасет «MNIST» (который скачивается по этой ссылке), состоящий из 60 тысяч картинок цифр с названиями; программный модуль, преобразующий данные из этого датасета и, собственно, код самой нейросети. Сначала — код модуля get-mnist:
# модули numpy и pathlib должны быть установлены в питон import numpy as np import pathlib # Объявление функции "get_mnist", которая будет возвращать значения, указанные ниже в команде return (images и labels). def get_mnist(): # Из файла извлекается два массива: images (из ключа “x_train”) и labels (из ключа “y_train”). x_train содержит изображения цифр, а y_train - соответствующие им метки (цифры от 0 до 9). # Примечание: предполагается, что файл mnist.npz размещён в папке data, которая находится в папке со скриптом. with np.load(f"{pathlib.Path(__file__).parent.absolute()}/data/mnist.npz") as f: images, labels = f["x_train"], f["y_train"] # Преобразуем тип данных массива images в float32 и сожмём значения в диапазон от 0 до 1 путем деления на 255. images = images.astype("float32") / 255 # images - трёхмерный массив двухмерных картинок, [0] измерение это количество картинок, а измерения [1] и [2] - размерности по высоте и ширине. Умножив размерности [1] и [2] друг на друга - получается общее количество пикселей в изображении. На выходе получается двухмерный массив - матрица. images = np.reshape(images, (images.shape[0], images.shape[1] * images.shape[2])) # Здесь метки преобразуются в формат "one-hot encoding". Мы создаем матрицу размером 10x10, где каждая строка представляет одну метку (цифру от 0 до 9). Значение 1 в строке соответствует метке, а остальные значения равны 0. labels = np.eye(10)[labels] # Функция возвращает два массива: images (обработанные изображения) и labels (one-hot encoded метки). return images, labelsДалее — разберём код самой нейросети — программы машинного обучения:
# Ниже код программы машинного обучения искусственной нейросети - классификатора изображений from data import get_mnist import numpy as np import matplotlib.pyplot as plt """ w = weights (вес), b = bias (смещение), i = input (ввод), h = hidden (скрытый), o = output (вывод), l = label (правильный ответ) e.g. w_i_h = weights from input layer to hidden layer (вес_ввод_скрытый - вес связи между вводом и скрытым слоем) """ # Загрузка данных из датасета images, labels = get_mnist() # Инициализация весов случайными числами w_i_h = np.random.uniform(-0.5, 0.5, (20, 784)) w_h_o = np.random.uniform(-0.5, 0.5, (10, 20)) # Инициализация смещений нулями b_i_h = np.zeros((20, 1)) b_h_o = np.zeros((10, 1)) # learn_rate - шаг обучения, epochs - количество итераций, nr_correct - отслеживает количество правильных предсказаний learn_rate = 0.2 nr_correct = 0 epochs = 3 # Цикл обучения for epoch in range(epochs): # Чтобы numpy мог посчитать скалярное произведение, необходимо подготовить одномерные матрицы img и l; использование zip ползволяет обрабатывать соответствующие элементы из нескольких массивов одновременно for img, l in zip(images, labels): img.shape += (1,) l.shape += (1,) # Прямое распространение ввод -> скрытый слой: вес смещения ввода суммируется с произведениями вводов и их весов (произведения между собой так же суммируются) h_pre = b_i_h + w_i_h @ img # Применение функции активации "сигмоида": np.exp - функция экспоненты - возведение числа Эйлера в степень, указанную в скобках. При возведении экспоненты в отрицательные степени - результат стремится к нулю, а при возведении в положительные - к бесконечности h = 1 / (1 + np.exp(-h_pre)) # Прямое распространение скрытый слой -> вывод: вес смещения скрытого слоя суммируется с произведениями значений узлов скрытого слоя и их весов (произведения между собой так же суммируются) o_pre = b_h_o + w_h_o @ h # Применение функции активации "сигмоида" o = 1 / (1 + np.exp(-o_pre)) # Стоимость ошибок, она же Функция потерь (на примере среднеквадратической ошибки; в данном коде эта переменная никак не используется и оставлена просто для наглядности) # e = 1 / len(o) * np.sum((l - o) ** 2, axis=0) # Если ячейка с максимальным значением в слое вывода совпадает с ячейкой с максимальным значением в одномерной матрице l - labels - то счётчик правильных ответов увеличивается nr_correct += int(np.argmax(o) == np.argmax(l)) # Обратное распространение вывод -> скрытый слой (производная функции потерь) delta_o = (2/len(o)) * (o - l) # К весу от скрытого слоя до вывода (на каждый нейрон соответственно) добавляется произведение его правильности на шаг обучения: у ошибочных результатов показатель отрицательный и, следовательно, он вычитается. # Транспонирование матриц - необходимо для их умножения. Операция умножения двух матриц выполнима только в том случае, если число столбцов в первом сомножителе равно числу строк во втором; в этом случае говорят, что матрицы согласованы. w_h_o += -learn_rate * delta_o @ np.transpose(h) b_h_o += -learn_rate * delta_o # Обратное распространение скрытый слой -> ввод (производная композитной функции - производная функции активации умноженная на производную функции потерь)) delta_h = np.transpose(w_h_o) @ delta_o * (h * (1 - h)) w_i_h += -learn_rate * delta_h @ np.transpose(img) b_i_h += -learn_rate * delta_h # Показать точность прогнозов для текущей итерации обучения и сбросить счётчик правильных прогнозов print(f"Уверенность: {round((nr_correct / images.shape[0]) * 100, 2)}%") nr_correct = 0 # Показать результаты - запрашивает у пользователя номер картинки, которая будет пропущена через нейросеть прямым распространением, в результате которого нейросеть предскажет какая цифра на картинке while True: index = int(input("Введите число (0 - 59999): ")) img = images[index] plt.imshow(img.reshape(28, 28), cmap="Greys") # Прямое распространение ввод -> скрытый слой h_pre = b_i_h + w_i_h @ img.reshape(784, 1) # Активация сигмоида h = 1 / (1 + np.exp(-h_pre)) # Прямое распространение скрытый слой -> вывод o_pre = b_h_o + w_h_o @ h # Активация сигмоида o = 1 / (1 + np.exp(-o_pre)) # argmax возвращает порядковый номер самого большого элемента в массиве plt.title(f"Нейросеть считает, что на картинке цифра {o.argmax()}") plt.show()Этот пример, с классификацией цифр датасета MNIST — можно считать «hello world» задачей из мира нейросетей — достаточно наглядный и при этом не слишком сложный код. Если его понимание не вызвало трудностей и интерес к теме сохранился — то можно дополнить список функций активации (начинайте с ReLU — она самая популярная) — wikipedia
Как же нейросети рисуют картинки?
Окей, с тем как нейросети классифицируют объекты — разобрались: даются вводные данные, даются эталонные данные и случайным образом инициализированные параметры «подстраиваются» под эти данные — в процессе обратного распространения ошибки.
Этот же принцип — но применённый не к параметрам, а к узлам первого слоя — слоя ввода — «лёгким движением руки» превращает обученную нейросеть-классификатор в генератор новых данных (например, изображений):
Значения узлов вводного слоя изменяются на произвольные величины (в данные вводится шум или же данные полностью заменяются шумом) — эти изменения передаются прямым распространением по обученной нейросети до выводов, которые меняются в соответствии с изменениями ввода, а по динамике этих изменений нейросеть отслеживает — какие изменения и на каких узлах ввода лучше соответствуют требуемым выводам, а какие — хуже. Таким образом — за каждую итерацию «примеряя» к требуемым выводам небольшие произвольные изменения данных (например, цвета пикселей) — отбрасывая не подходящие значения и закрепляя подходящие — нейросеть за множество итераций формирует наиболее подходящий под запрошенные выводы результат, как бы «рассеивая» перекрывающий его шум:
Например — у нас есть нейросеть-классификатор, обученная на различных изображениях, среди которых есть подсолнух. Показывая нейросети этот (или любой другой) подсолнух — мы вводим определённую последовательность данных (пиксели на входном слое нейросети), которые, проходя по всем узлам и параметрам, больше всего разрешаются в класс, соответствующий «подсолнуху». Само собой — на разные изображения «реакция» нейросети будет отличаться — условно говоря, относительно нейросети, всегда будут какие-то более «подсолнечные» подсолнухи, чем остальные, ничем не худшие — просто снятые с другого ракурса и расстояния.
Вводя в изображение подсолнуха минимальный шум — изменяя значения пикселей на случайные, но небольшие величины — будет меняться и уверенность нейросети в том, что перед ней всё ещё подсолнух. Постоянно добавляя к изображению шум, рано или поздно — исходное изображение станет совершенно неразличимым — изнутри нейросети это выглядит как наименьшая уверенность — наименьший вывод по всем классам.
Ну а теперь, «дело за малым» — взять да обратить процесс добавления шума примерно так же, как описанное выше обратное распространение обращает прямое.
Но ведь шум то — случайный! Обратить такое — уже не так просто, как обратить функцию по производной. Любой случайный шум — безвозвратно удаляет часть информации; удаление такого случайного шума либо восстановление информации — задача, по крайней мере до недавних пор, весьма нетривиальная. Для решения такой задачи необходим алгоритм, во-первых, отличающий «шум» от «не шум» и, во-вторых, алгоритм, заменяющий найденный им шум на данные, наиболее подходящие по контексту. Как бы к этой задаче подошёл человек? А давайте проверим — как же хорошо что перед монитором как раз есть такой!
На какой картинке больше шума? Очевидно — что шума больше на правой. Очевидно, но почему? Чем мы можем характеризовать помехи? Давайте посмотрим на картинки поближе:
Ну пиксели и пиксели. Если взять два увеличенных участка с этих картинок и сравнить между собой — будет ли так же очевидно, на каком из них больше шума, как это очевидно с исходниками? А вот и нет! В случае с увеличенными фрагментами — перед нами могут быть две примерно одинаково хаотичных картинки.
Выходит, что шум — это что-то вроде меры хаоса в системе и получается, что эта мера напрямую зависит от масштабов этой системы: в масштабах увеличенных фрагментов 10х10 пикселей мера хаоса оказывается выше, чем в их исходниках значительно большего размера. Почему? Как ни банально — просто потому, что больший масштаб всегда содержит в себе больше смысловых элементов, чем меньший. Именно с общего плана мы видим, что слева — перед нами горы, камни, вода, небо, а справа — люди и воздушные шары. И лишь поняв это, для нас становится очевидным, что на горах слева намного меньше шума чем на шариках справа — потому что мы сравниваем найденные на изображениях образы с эталонными (воспоминаниями), откуда и делаем вывод, по количеству отличий — о количестве шума. Всё дело в том, что мы знаем — как обычно выглядят и люди, и шарики, и горы — это и позволяет нам видеть на их изображениях «лишние» пиксели шума.
Точно такой же принцип используется в «свёрточных нейросетях«, наиболее широко известных под термином CNN (convolutional neural network), самым популярным примером которых является архитектура U-Net (потому что её схема похожа на букву U): они смотрят на общий план, выделяют на нём смысловые элементы и сравнивают их с накопленным (в ходе обучения) опытом. То что мы назвали бы «посмотреть на картинку шире», в предыдущем примере, отдаляясь от мелкого фрагмента к полному изображению — в терминологии нейросетей называется «свёрткой» (convolution). В процессе свёртки нейросеть, как бы, абстрагируется от отдельных пикселей к смыслу отдельных элементов — а затем, смыслу всей картинки в целом. Свёртка — это, по сути, сжатие (с потерями), в некотором смысле, напоминающее вычисление хеш-суммы: в процессе свёртки исходные данные сжимаются в сотни и тысячи раз (и больше), при этом — у различных исходных данных свёртки так же будут разные (до определённого уровня). Сопоставлять объекты по их «свёрткам» — намного быстрее, чем сравнивать несжатые объекты. Благодаря этому «лайфхаку» нейросети могут в реальном времени обрабатывать визуальные данные даже на чипах мобильных устройств — современных смартфонов.
Значит, берём нейросеть-классификатор — и дополняем свёрткой, если заведомо её там не было. И тут мы понимаем, что классификатор и свёртка — это одно и то же? По сути — как классификатор превращает сотни вводов в десяток выводов — так же может и свёртка, так в чём разница? Разница в параметрах: у сети-классификатора параметры инициализируются случайным образом, после чего итеративно меняются на шаг обучения и вычисленный с помощью цепного правила градиент; а у свёрточных сетей — все параметры «преднастроены с завода» таким специальным образом, который «одним выстрелом бьёт двух зайцев»: и данные сжимает, сохраняя при этом смысловые элементы — и выделяет границы перехода (между смысловыми элементами). Да что уж тут говорить — этот метод настолько «волшебный», что с его помощью можно даже нарисовать тени на плоских (двухмерных) объектах! Задумывались когда-нибудь, как работают эти прикольные фильтры в фотошопе? А вот как:
U-Net: процесс свёртки. Слева — ввод, посередине — фильтр (ядро свёртки), справа — свёртка. Процесс можно повторять и для свёрток, в результате — сжимая исходник насколько угодно. Как видно из анимации выше — работает оно предельно просто: по всей матрице ввода проходит маска (ядро свёртки) с произвольными значениями, функция которой — умножать значения в подлежащих ячейках матрицы на свои, а затем суммировать эти значения по всему ядру в одно результирующее значение для ячейки матрицы свёртки. Как не трудно догадаться — степень сжатия зависит от размера ядра свёртки и количества итераций. Матрицу любого исходного размера можно сжать до размера ядра свёртки — за определённое число итераций.
Так выглядит свёртка на конкретном примере (взял из этой статьи):
Слева — оригинал; справа — свёртка. Конкретно это ядро свёртки — увеличивает разницу значений на границах — так называемый «фильтр улучшения чёткости». Матрица этого ядра свёртки — на картинке ниже. 
Слева — оригинал; справа — сумма «горизонтальной» и «вертикальной» свёрток -
Маски Собеля:
Маски Собеля дают менее точные грани, чем более «шустрый» аналог Робертса, зато — они менее чувствительны к шуму, благодаря чему на практике более популярны. Ядра свёртки Робертса и Собеля — используются для выделения граней на изображениях. Каждая маска по отдельности — выделяет только вертикальные, либо горизонтальные грани; на примерах выше изображены суммы этих свёрток — выделяющие все грани в совокупности.
Если нужно именно сжать исходник, а не выделить грани, то есть и более простой алгоритм свёртки — «Max Pooling» — он просто сохраняет максимальное значение из каждой группы, удаляя все остальные; за один «проход» сжимая исходник в 4 и более раз:
Max Pooling — самый популярный способ «пулинга» в нейросетях. Пулинг максимального — самый распространённый, но далеко не единственный способ пулинга — методов придумали множество, есть среди них и довольно нетривиальные — такие как генетический или пирамидный.
Уместен вопрос — почему пулинг максимальных значений более популярен, чем пулинг, например, средних?
сжав картинку в 65 раз, max pooling лучше сохранил усы, а average pooling — лучше сохранил глаза Как видно на сравнении выше — пулинг исходных изображений может привести к довольно непредсказуемым результатам — поэтому обычно пулингу подвергают не исходники, а их свёртки с выделенными переходами:
другое дело! и усы, и глаза — всё на своих местах, да и разницы практически нет, но max pooling всё же чуточку лучше? А вот и та самая U-Net, на схеме которой синими стрелочками изображены операции выделения граней свёрткой, а красными — пулинг максимальных значений:
при ядре свёртки 3х3, размерности слоёв свертки всегда различаются на 2 по горизонтали и 2 по вертикали. Если нет — значит используется «костыль», расширяющий матрицы за их границы. Вернёмся к вопросу распознания и коррекции шума. Если на вход свёрточной нейросети подать картинку с сильными помехами — то свёртка такой нейросети масками, выделяющими границы перехода — лишь усугубит ситуацию: перед такими свёртками, входные данные уместно пропустить через «медианный фильтр» — обычно это маска 3х3 или 5х5, на выходе у которой — самое среднее значение из всех, которые попадают в область маски. Медианный фильтр — эффективное средство подавления случайных аномалий (шума) — но это всё ещё ни в коем смысле не средство восстановления информации.
Слева — оригинал; справа — медианный фильтр 7х7. Помните магнитофоны VHS?
свёртка оригинала и свёртка после медианного фильтра: на последней значительно меньше граней относятся к шуму и больше — к реальным объектам Так о чём это я? Ах да — нейросети, превращающие текст в картинки. Но какой текст? Где условия? Неужели можно ввести прямо таки что угодно, и нейросеть нарисует?! Лукавят, лукавят эти «дата саентисты»! Что, с руками проблемы? Пальцев многовато? Нет, не туда смотрите. Пальцы, волосы, прочие неестественности — это всё ерунда, которая легко решается нормальным датасетом, архитектурой и обучением. То что нейросети уделяют недостаточно внимания каким-то деталям — как говорится, «дело техники» — пройдёт несколько лет и эти проблемы уйдут в историю. Но есть кое-что, чего «диффузионные» модели не нарисуют — хоть ты тресни! Уже догадываетесь? Вот вам несколько запросов и их результаты — от одной из самых популярных и самых «мощных» среди публично доступных нейросетей — stable diffusion:
«noisy image of mountains» Я вижу «mountains», но я не вижу «noise». Может быть, я не так смотрю, или не тот запрос? Попробуем другой, чуть более конкретный:
«very bad quality, noisy image, very rubbish and low resolution picture of nature, add noise effect» И снова неудача! Снова — из всего моего длинного и очень конкретно сформулированного запроса, нейросеть поняла только одно слово «nature».
Не сдаёмся (пока бесплатные генерации ещё есть):
«show two images of same place but one with noise and other without noise to show the difference between noisy image and clear image» Запрошенной разницы между картинками — лично я не вижу. По моему скромному мнению — количество шума с обоих сторон одинаковое.
Широко известная нейросеть ChatGPT прекрасно понимает, что такое пиксели, что такое шум и в чём разница между картинкой низкого разрешения и картинкой высокого разрешения. Современные, хорошо обученные нейросети это понимают, но результат дать не могут. В чём же дело?
А дело всё в последнем элементе таких вот генеративных нейросетей — благодаря которому их полное название — это «Генеративно-состязательные сети» (Generative adversarial network, сокращённо GAN): речь о том самом состязательном элементе этих нейросетей, назначение которого — «лупить палкой» по генератору, если его «творение» хоть чем-то не устраивает состязателя. Именно поэтому генеративно-состязательные сети показывают нам «залипательные» картинки, как бы «отполированные» до блеска — но не могут показать нам всё то, что «состязатель» помечает «браком»; а бракует он всё то, что вызывает у него «смешанные» чувства — все результаты, которые вызывают у нейросети недостаточную (достаточность задаётся администратором) активацию по классам, соответствующим текстовому запросу пользователя, или же если активация по «плохим» классам (тоже задаются администратором) слишком высокая — например, если у нейросети-классификатора, выступающего в роли состязателя, глядя на выдачу псевдослучайного генератора возникает слишком высокий вывод по классу «шум» — то такой состязатель попросит генератор изменить выдачу — но не сильно — на небольшой определённый шаг. Генератор, по запросу состязателя, внесёт изменения в данные на входном слое нейросети (который теперь, как бы, в роли вывода, где формируется картинка) — на что состязатель ему скажет — в правильном ли направлении были внесены изменения или нет. Если направление не правильное — оно меняется, если правильное — то дополняется. Вот так — внося небольшие изменения в данные (например, в цвет пикселей), за множество итераций, данные на входном слое нейросети подгоняются под максимальный вывод по запрошенным классам и минимальный вывод по «запрещённым» классам (которые обычно связаны с шумом и низким качеством изображения, цензурным контентом и «счётчиком пальцев» — в качестве примера).
Выходит, что раньше — нейросети рисовали руки со странными количествами пальцев, теперь же — не могут выполнить запрос, требующий нарисовать руку с числом пальцев, отличным от пяти:
из крайности в крайность… Точно так же нейросети и «восстанавливают» информацию, удалённую шумом: строго говоря, они её не восстанавливают, а заменяют наиболее подходящей по контексту. В отличие от медианного фильтра, по сути просто «размазывающего» картинки, заменяющего шум средним значением окружающих его пикселей — генеративно-состязательная нейросеть работает с пикселями отдельно от их соседей — но в связке со смысловым содержанием, которое они учатся выделять с помощью обучения размеченным датасетом. Благодаря такой логике, нейросети могут восстанавливать тонкие линии (даже в один пиксель) и увеличивать разрешение изображений, «врисовывая» между пикселями исходного изображения наиболее подходящие по смыслу:
лица людей содержат «черты» — мельчайшие элементы, на которые заточено человеческое восприятие, поэтому «апскейлинг» (upscaling) в случае с людьми работает… так себе
Источники и ссылки на дополнительные материалы по теме:
Плагин для Экселя «Nerual Excel» — youtube
Матричные фильтры обработки изображений — хабр
Pooling In Convolutional Neural Networks
Pooling Methods in Deep Neural Networks, a Review — PDF
Классификатор изображений на питоне — youtube
Ряды Тейлора и ряды Фурье в нейросетях — youtube
Оптимизация программы обучения — стохастический градиентный спуск — wikipedia
Свёрточные нейронные сети — wikipedia
ну и моя «записная книжка» — в телеграммеПублично доступные GAN (авторизация аккаунтом гугл):
https://wepik.com/ai-generate
https://stablediffusionweb.com/
P.S. не советую верить всяким лицам нетрадиционной ориентации, особенно — в части их прогнозов на скорейшее пришествие искусственного сверх-интеллекта и прочих «сингулярностей» — на сегодняшний день это просто смехотворно. Сэм Альтман выдавливает все соки из своего «хайпа», со временем всё больше напоминая эдакую Грету Тундберг в мире нейросетей. Туда же — всех нытиков, кто сетует на нейросети «лишающие» их работы.
Есть лишь один способ угомонить этих «недо-эко-активистов» и остальных плоскоземельщиков — игнорировать их и деликатно презирать всех кто ещё не делает этого. От всей души прошу. Просвещения и всех благ дочитавшим.
-
ссылка на оригинал статьи https://habr.com/ru/articles/805209/
Добавить комментарий