Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
Визуализация Наглядно представить концепцию вложенной функции можно с помо- щью рисунка. Вложенные функции 25 На рис. 1.6 мы видим, что данные подаются в первую функцию, преобра- зуются, выводятся и становятся входными данными для второй функции, которая и дает окончательный результат. f 1 x f 2 y Рис. 1.6. Вложенные функции Математическое представление В математической нотации вложенная функция выглядит так: Такое представление уже сложно назвать интуитивно понятным, потому что читать эту запись нужно не по порядку, а изнутри наружу. Хотя, ка- залось бы, это должно читаться как «функция f 2 функции f 1 переменной x», но на самом деле мы вычисляем f 1 от переменной x, а затем — f 2 от полученного результата. Код Чтобы представить вложенные функции в виде кода, для них первым делом нужно определить тип данных: from typing import List # Function принимает в качестве аргумента объекты ndarray и выводит # объекты ndarray Array_Function = Callable[[ndarray], ndarray] # Chain — список функций Chain = List[Array_Function] Теперь определим прохождение данных по цепочке из двух функций: def chain_length_2(chain: Chain, a: ndarray) -> ndarray: ''' Вычисляет подряд значение двух функций в объекте "Chain". 26 Глава 1. Математическая база ''' assert len(chain) == 2, \ "Длина объекта 'chain' должна быть равна 2" f1 = chain[0] f2 = chain[1] return f2(f1(x)) Еще одна визуализация Так как составная функция, по сути, представляет собой один объект, ее можно представить в виде f 1 f 2 , как показано на рис. 1.7. f 1 f 2 x y Рис. 1.7. Альтернативное представление вложенных функций Из математического анализа известно, что если все функции, из которых состоит составная функция, дифференцируемы в рассматриваемой точке, то и составная функция, как правило, дифференцируема в этой точке! То есть функция f 1 f 2 — это просто обычная функция, от которой можно взять производную, — а именно производные составных функций лежат в основе моделей глубокого обучения. Для вычисления производных сложных функций нам потребуется фор- мула, чем мы и займемся далее. Цепное правило Цепное правило (или правило дифференцирования сложной функции) в математическом анализе позволяет вычислять производную компо- зиции двух и более функций на основе индивидуальных производных. С математической точки зрения модели глубокого обучения представ- ляют собой составные функции, а в следующих главах вы увидите, что понимание того, каким способом берутся производные таких функций, потребуется для обучения этих моделей. Цепное правило 27 Математическое представление В математической нотации теорема утверждает, что для значения x , где u — вспомогательная переменная, представляющая собой входное значение функции. Производную функции f одной переменной можно обозначить как . Вспомогательная переменная в данном случае может быть любой, ведь запись и означает одно и то же. Я обозначил эту переменную u. Но позднее нам придется иметь дело с функциями нескольких пере- менных, например x и y. И в этом случае между и уже будет принципиальная разница. Именно поэтому в начале раздела мы записали производные с по- мощью вспомогательной переменной u и будем в дальнейшем ис- пользовать ее для производных функций одной переменной. Визуализация Формула из предыдущего раздела не слишком помогает понять суть цепного правила. Давайте посмотрим на рис. 8, иллюстрирующий, что же такое производная в простом случае f 1 f 2. Из рисунка интуитивно понятно, что производная составной функции должна представлять собой произведение производных входящих в нее функций. Предположим, что производная первой функции при u = 5 дает значение 3, то есть Затем предположим, что значение первой функции при величине вход- ного параметра 5 равно 1, то есть . Производную этой функции при u = 1 приравняем к –2, то есть 28 Глава 1. Математическая база f 1 5 1 меньшая единица 1 f 2 1 4 f 1 f 2 5 4 1 меньшая единица – 3 меньших единицы 2 меньших единицы 1 меньшая единица –6 меньших единиц Рис. 1.8. Цепное правило Теперь вспомним, что эти функции связаны друг с другом. Соответствен- но если при изменении входного значения второго черного ящика на 1 мы получим на выходе значение –2, то изменение входного значения до 3 даст нам изменение выходного значения на величину –2 × 3 = –6. Именно поэтому в формуле для цепного правила фигурирует произ- ведение: Как видите, рассмотрение математической записи цепного правила с ри- сунком позволяет определить выходное значение вложенной функции по ее входному значению. Теперь посмотрим, как может выглядеть код, вычисляющий значение такой производной. Код Первым делом напишем код, а потом покажем, что он корректно вычис- ляет производную вложенной функции. В качестве примера рассмотрим уже знакомые квадратичную функцию и сигмоиду, которая применяется в нейронных сетях в качестве функции активации: def sigmoid(x: ndarray) -> ndarray: ''' Применение сигмоидной функции к каждому элементу объекта ndarray. ''' return 1 / (1 + np.exp(-x)) Цепное правило 29 А этот код использует цепное правило: def chain_deriv_2(chain: Chain, input_range: ndarray) -> ndarray: ''' Вычисление производной двух вложенных функций: (f2(f1(x))' = f2'(f1(x)) * f1'(x) с помощью цепного правила ''' assert len(chain) == 2, \ "Для этой функции нужны объекты 'Chain' длиной 2" assert input_range.ndim == 1, \ "Диапазон входных данных функции задает 1-мерный объект ndarray" f1 = chain[0] f2 = chain[1] # df1/dx f1_of_x = f1(input_range) # df1/du df1dx = deriv(f1, input_range) # df2/du(f1(x)) df2du = deriv(f2, f1(input_range)) # Поэлементно перемножаем полученные значения return df1dx * df2du На рис. 1.9 показан результат применения цепного правила: PLOT_RANGE = np.arange(-3, 3, 0.01) chain_1 = [square, sigmoid] chain_2 = [sigmoid, square] plot_chain(chain_1, PLOT_RANGE) plot_chain_deriv(chain_1, PLOT_RANGE) plot_chain(chain_2, PLOT_RANGE) plot_chain_deriv(chain_2, PLOT_RANGE) 30 Глава 1. Математическая база Графики функции f(x) = sigmoid(square(x)) и ее производной Графики функции f(x) = square(sigmoid(x)) и ее производной Рис. 1.9. Результат применения цепного правила Кажется, цепное правило работает. Там, где функция на- клонена вверх, ее производная положительна, там, где она параллельна оси абсцисс, производная равна нулю; при наклоне функции вниз ее производная отрицательна. Как математически, так и с помощью кода мы можем вы- числять производную «составных» функций, таких как f 1 f 2 , если обе эти функции дифференцируемы. С математической точки зрения модели глубокого обучения представ- ляют собой цепочки из функций. Поэтому сейчас мы рассмотрим более длинный пример, чтобы в дальнейшем вы смогли экстраполировать эти знания на более сложные модели. Более длинная цепочка Возьмем три дифференцируемых функции f 1 , f 2 и f 3 и попробуем вы- числить производную f 1 f 2 f 3 . Мы уже знаем, что функция, составленная из любого конечного числа дифференцируемых функций, тоже диффе- ренцируема. Более длинная цепочка 31 Математическое представление Дифференцирование происходит по следующей формуле: Здесь работает та же схема, что и в случае цепочки из двух функций , но формула не позволяет интуитивно понять, что именно происходит! Визуализация Лучшее представление о том, как работает эта формула, нам даст рис. 1.10. f 1 5 f 1 (x) f 2 f 2 (f 1 (x)) f 3 f 1 '(x) f 2 '(f 1 (x)) f 3 '(f 2 (f 1 (x))) y Рис. 1.10. Вычисление производной трех вложенных функций Ход рассуждений в данном случае будет таким же, как и ранее. Пред- ставим, что все три функции как бы нанизаны на струну, входное значе- ние функции f 1 f 2 f 3 обозначим a, а выходное — b. При изменении a на небольшое значение Δ результат f 1 (a) изменится на , умноженное на Δ. Следующий шаг в цепочке — функция f 2 (f 1 (x)) изменится на , умноженное на Δ. Аналогичным образом рассчитыва- ется изменение на третьем шаге. Это будет полная формула, написанная в соответствии с цепным правилом и умноженная на Δ. Потратьте неко- торое время на изучение этого принципа. А когда мы начнем писать код, многие вещи постепенно прояснятся. 32 Глава 1. Математическая база Код Теперь напишем код вычисления производной по приведенной в пре- дыдущем разделе формуле. Что интересно, на этом простом примере мы уже видим отправной момент, с которого начинается процесс прямого и обратного распространения по нейронной сети: def chain_deriv_3(chain: Chain, input_range: ndarray) -> ndarray: ''' Вычисление производной трех вложенных функций: (f3(f2(f1)))' = f3'(f2(f1(x))) * f2'(f1(x)) * f1'(x) с помощью цепного правила ''' assert len(chain) == 3, \ "Для этой функции нужны объекты 'Chain' длиной 3" f1 = chain[0] f2 = chain[1] f3 = chain[2] # f1(x) f1_of_x = f1(input_range) # f2(f1(x)) f2_of_x = f2(f1_of_x) # df3du df3du = deriv(f3, f2_of_x) # df2du df2du = deriv(f2, f1_of_x) # df1dx df1dx = deriv(f1, input_range) # Поэлементно перемножаем полученные значения return df1dx * df2du * df3du Более длинная цепочка 33 При вычислении производной вложенных функций по цепному правилу происходит интересная вещь. Дело в том, что эта операция осуществля- ется в два этапа: 1. Сначала мы идем вперед, вычисляя значения f1_of_x и f2_of_x . Это можно назвать прямым проходом (forward pass). 2. Затем эти значения используются для расчета компонентов произ- водной. При этом мы идем в обратную сторону. В конце мы перемножаем эти значения и получаем производную. Теперь на примере трех уже определенных нами функций — sigmoid , square и leaky_relu — посмотрим, как все работает. PLOT_RANGE = np.range(-3, 3, 0.01) plot_chain([leaky_relu, sigmoid, square], PLOT_RANGE) plot_chain_deriv([leaky_relu, sigmoid, square], PLOT_RANGE) Результат показан на рис. 1.11. sigmoid(square(leakyrelu(X))) sigmoid(square(leakyrelu(X))) Рис. 1.11. Для трех вложенных функций цепное правило тоже работает Сравнив графики производных с наклонами исходных функций, мы убедимся в корректной работе цепного правила. Теперь можно рассмотреть сложные функции нескольких переменных. Они подчиняются тем же самым принципам и намного больше подходят для описания того, что происходит во время глубокого обучения. 34 Глава 1. Математическая база Функции нескольких переменных К этому моменту вы должны были понять, как формируются сложные функции, и уметь представлять эти функции в виде набора блоков с вход- ными и выходными данными. Выше я показал как формулы для взятия производных таких функций, так и поэтапный процесс их вычисления с прямым и обратным проходами. Функции, с которыми приходится иметь дело в глубоком обучении, часто имеют целый набор входных данных, которые в процессе обработки скла- дываются, умножаются или комбинируются каким-то другим способом. Вычислить производную такой функции тоже несложно. В качестве при- мера рассмотрим простой сценарий: функция двух переменных, которая вычисляет их сумму и затем передает в другую функцию. Математическое представление В данном примере лучше начать с математики. Пусть входные данные представляют переменные x и y. Действие функции можно разбить на два этапа. Сначала функция, которую мы обозначим греческой буквой α, выполняет сложение входных данных. Греческие буквы и дальше будут использоваться в качестве имен функций. Результат действия функции обозначим a. С формальной точки зрения все очень просто: Затем передадим a в некую функцию σ (это может быть любая непре- рывная функция, например сигмоид, квадратичная функция или другая функция по вашему выбору). Результат ее работы обозначим переменной s: При этом ничто не мешает обозначить всю функцию f и написать: С математической точки зрения это более точная запись, но она не дает представления о том, что на самом деле мы последовательно выполняем две операции. Лучше всего это будет видно на рисунке, который при- веден ниже. Функции нескольких переменных 35 Визуализация Теперь, когда мы добрались до функций нескольких переменных, окон- чательно стало понятно, что наши схемы со стрелками, указывающими на порядок выполнения операций, представляют собой вычислительные графы (computational graphs). Например, на рис. 1.12 показан вычисли- тельный граф описанной выше функции f. α x y a σ s Рис. 1.12. Функция двух переменных Мы видим, что в функцию α подаются два элемента входных данных, а результат ее работы a передается в функцию σ. Код Код вычислений в данном случае выглядит очень просто, но обратите внимание на дополнительный проверочный оператор: def multiple_inputs_add(x: ndarray, y: ndarray, sigma: Array_Function) -> float: ''' Функция сложения двух переменных, прямой проход. ''' assert x.shape == y.shape a = x + y return sigma(a) Эта функция не похожа на те, с которыми мы имели дело раньше. Если в предыдущих случаях наши функции по отдельности обрабатывали каждый элемент объекта ndarray , то теперь мы сначала проверяем фор- му этих объектов, чтобы удостовериться, что с ними можно проводить указанную в коде операцию. Такие проверки всегда выполняются для функций нескольких переменных. Для такой простой операции, как сло- жение, достаточно проверить идентичность форм. Только в этом случае мы сможем выполнять операцию поэлементно. 36 Глава 1. Математическая база Производные функций нескольких переменных Вряд ли вас удивит тот факт, что производную такой функции можно брать по обеим ее переменным. Визуализация По сути, мы делаем то же самое, что и в случае функции одной переменной: идем назад по нашему графу, вычисляя производные всех составляющих функций, а затем получаем производную путем перемножения резуль- татов (рис. 1.13). α ∂ α ∂ x x y a ((x,y)) σ s ∂ σ ∂ y (a) Рис. 1.13. Обратный проход через вычислительный граф функции нескольких переменных Математическое представление К функциям нескольких переменных тоже применимо цепное правило. Так как в рассматриваемом случае речь идет о вложенной функции , то получаем: Результат дифференцирования по второй переменной будет иден- тичным. Теперь обратите внимание, что: , так как для каждой единицы приращения по x при любых значениях x приращение α тоже составляет единицу (это же справедливо и для y). Функции нескольких переменных с векторными аргументами 37 С учетом сказанного можно написать код, вычисляющий производную такой функции. Код def multiple_inputs_add_backward(x: ndarray, y: ndarray, sigma: Array_Function) -> float: ''' Вычисление производной нашей простой функции по обеим переменным. ''' # "Прямой проход" a = x + y # Вычисление производных dsda = deriv(sigma, a) dadx, dady = 1, 1 return dsda * dadx, dsda * dady В качестве самостоятельной работы рассмотрите такую же функцию, в которой переменные x и y не складываются, а перемножаются. А мы перейдем к более сложному примеру, который практически полно- стью воспроизводит происходящее в глубоком обучении. От функции, рассмотренной в этом разделе, она будет отличаться только векторными аргументами. Функции нескольких переменных с векторными аргументами В глубоком обучении используются функции, на вход которых подаются векторы или матрицы. Эти объекты можно складывать, перемножать, а для векторов существует еще и такая операция, как скалярное произ- ведение. Ниже мы рассмотрим примеры применения к этим функциям цепного правила. 38 Глава 1. Математическая база Именно эти техники позволяют понять, почему работает глубокое об- учение. Основная цель глубокого обучения — подбор модели, наилучшим образом описывающей данные. Фактически мы ищем функцию, которая точнее всего сможет сопоставить результаты наблюдений (служащие ее входными данными) с определенным шаблоном (который играет роль выходных данных). Входные данные удобно представлять в виде матриц, строки которых содержат результаты наблюдений, а столбцы — соответствующие количественные характеристики. Подробно это будет обсуждаться в следующей главе, а пока важно понять математическую базу происходящего, то есть научиться вычислять производные сложных функций, в которых происходит скалярное умножение векторов или умножение матриц. Математическое представление При работе с нейронными сетями единицы информации или результаты наблюдений обычно представляют в виде набора признаков. Каждый признак обозначается как x 1 , x 2 и т. д., до последнего признака x n : Например, в следующей главе мы построим нейронную сеть, которая будет прогнозировать цену на жилье; в этом примере x 1 , x 2 и далее — это числовые характеристики, такие как площадь дома или его расстояние до ближайшей школы. |