Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
Создание новых признаков из уже существующих Возможно, самая распространенная операция в нейронных сетях — опре- деление взвешенной суммы признаков. Именно этот параметр усиливает определенные признаки и снимает акцент с других. Его можно рассма- тривать как новую функцию, полученную комбинированием существую- щих. Математически это скалярное произведение вектора характеристик и имеющего такой же размер вектора весов: w 1 , w 2 и дальше до w n . Давайте рассмотрим эту концепцию более подробно. Создание новых признаков из уже существующих 39 Математическое представление Если определить вектор весов отдельных признаков как , скалярное произведение двух векторов будет выглядеть так: Обратите внимание, что эта операция представляет собой частный слу- чай умножения матриц. Фактически мы умножаем вектор-строку X на вектор-столбец W. Теперь посмотрим, как изобразить это графически. Визуализация Простой способ изображения скалярного произведения показан на рис. 1.14. γ γ = Умножение матриц [ x ] [ y ] N Входные данные Выходные Рис. 1.14. Скалярное произведение двух векторов Мы видим, что функция принимает два аргумента, роль которых могут играть объекты ndarray , и превращает их в один объект ndarray Но работу функций нескольких переменных можно представить и дру- гими способами. Например, выделив отдельные операции и входные данные, как показано на рис. 1.15 и 1.16. 40 Глава 1. Математическая база Промежуточные значения x 1 w 1 M x 2 w 2 M A x 3 w 3 M M 1 M 2 N M 3 Рис. 1.15. Другой способ представления операции умножения матриц x 1 w 1 x 2 w 2 F x 3 w 3 N Рис. 1.16. Третий способ предоставления умножения матриц Дело в том, что произведение матриц, как и его частный случай, скаляр- ное произведение, — это краткое представление для набора отдельных операций, которое, как мы увидим в следующем разделе, сокращает и процедуру вычисления производных. Код И наконец, несложный код, реализующий операцию умножения: def matmul_forward(X: ndarray, W: ndarray) -> ndarray: ''' Прямой проход при умножении матриц. ''' assert X.shape[1] == W.shape[0], \ ''' Для операции умножения число столбцов первого массива должно совпадать с числом строк второго; у нас же число столбцов первого массива равно {0}, а число строк второго равно {1}. ''' Производные функции нескольких векторных переменных 41 .format(X.shape[1], W.shape[0]) # умножение матриц N = np.dot(X, W) return N Этот код содержит оператор проверки, гарантирующий допустимость операции умножения. Раньше такая проверка не требовалась, так как мы имели дело с объектами ndarray одинакового размера, операции над которыми выполнялись поэлементно. Производные функции нескольких векторных переменных Производная функции одной переменной, например или , находится очень легко — достаточно применить соот- ветствующее правило. А как должна выглядеть производная векторных функций? Обозначим скалярное произведение и попробуем ответить на вопрос, что такое и Визуализация По сути, мы хотим сделать что-то, представленное на рис. 1.17. γ γ = Умножение матриц [ x ] [ y ] N Входные данные N = выходные ∂ γ ∂ x ∂ γ ∂ y Рис. 1.17. Обратный проход операции умножения матриц В случае обычных операций сложения и умножения такие производные вычисляются просто, как вы видели в ранее рассмотренных примерах. 42 Глава 1. Математическая база Но что делать в случае умножения матриц? Давайте посмотрим, как это выглядит математически. Математическое представление Первым делом следует определить «производную по матрице». Если вспомнить, что матрица — это удобная форма представления набора чи- сел, легко понять, что производная по матрице означает производную по каждому из ее элементов. Для вектора-строки X она будет выглядеть так: Но функция ν дает число: . Посмотрев на него внимательно, мы увидим, что изменение x 1 на ϵ единиц меняет N на w 1 ×ϵ единиц. То же самое происходит с остальными x i элементами. Поэтому: , , В результате получаем: Этот удивительно элегантный результат дает ключ к тому, почему глубокое обучение работает и может быть так точно реализовано. Используя аналогичные рассуждения, получим: Производные векторных функций: продолжение 43 Код Сложную часть рассуждений, то есть математический вывод ответа, мы уже проделали. Написать код легко: def matmul_backward_first(X: ndarray, W: ndarray) -> ndarray: ''' Обратный проход для операции умножения матриц по первому аргументу. ''' # обратный проход dNdX = np.transpose(W, (1, 0)) return dNdX Рассчитанный здесь параметр dNdX представляет собой частную произ- водную каждого элемента вектора X по скалярному произведению N. Этот показатель мы в дальнейшем будем называть градиентом X. Дело в том, что каждому элементу вектора X, например x 3 , в массиве dNdx соответствует элемент (в нашем случае это dNdX [2] ), представляющий собой частную производную скалярного произведения N по х 3 . Далее в книге термин «градиент» будет обозначать многомерный аналог частной производной; точнее говоря, массив частных производных функции по каждому из ее аргументов. Производные векторных функций: продолжение Разумеется, модели глубокого обучения включают в себя целые цепочки операций как с векторными аргументами, так и с поэлементной обработ- кой поданного на вход массива ndarray . Поэтому сейчас мы рассмотрим процесс вычисления производной составной функции, которая включает в себя оба вида аргументов. Предположим, что функция ν(X, W) вычис- ляет скалярное произведение векторов X и W и передает полученный результат в функцию σ. Мы хотим получить ее частные производные, или, если использовать новую терминологию, вычислить градиент этой новой функции по переменным X и W. Уже в следующей главе я подробно расскажу, как это связано с работой нейронных сетей, а пока же мы про- сто учимся находить градиенты вычислительных графов произвольной сложности. 44 Глава 1. Математическая база Визуализация Схема на рис. 1.18 отличается от схемы на рис. 1.17 только добавленной в конец функцией σ. γ [x] [w] N S δ Рис. 1.18. Уже знакомая схема, к которой добавлена еще одна функция Математическое представление Формула такой функции выглядит очень просто: Код Теперь напишем ее код: def matrix_forward_extra(X: ndarray, W: ndarray, sigma: Array_Function) -> ndarray: ''' Вычисление функции, в которой результат умножения матриц передается в следующую функцию. ''' assert X.shape[1] == W.shape[0] # умножение матриц N = np.dot(X, W) # подаем результат умножения матриц на выход функции сигма S = sigma(N) return S Вычисление производной Обратный проход в этом случае представляет собой всего лишь небольшое расширение предыдущего примера. Производные векторных функций: продолжение 45 Математическое представление Так как — это вложенная функция, то есть , ее частная производная, например, по X должна выглядеть так: Но первая часть этого уравнения — это всего лишь: Так как σ — непрерывная функция, производную которой можно вычислить в любой точке, просто подставим в нее значение В предыдущем разделе мы вывели, что . Сделаем подста- новку и получим: Как и в предыдущем примере, появляется вектор, совпадающий по направ- лению с вектором X, что в финале дает число , умноженное на вектор-строку. Визуализация Схема из рис. 1.19 обратного прохода рассматриваемой функции напо- минает схему из предыдущего примера, которую вы видели на рис. 1.17. Просто сейчас появился еще один множитель. Он представляет собой производную функции σ, оцененную для значения, которое мы получили в качестве результата умножения матриц. γ [x] [w] N (N) S σ ∂ γ ∂ x ∂ γ ∂ w ∂ σ ∂ u Рис. 1.19. Обратный проход для векторной составной функции 46 Глава 1. Математическая база Код Код для обратного прохода выглядит очень просто: def matrix_function_backward_1(X: ndarray, W: ndarray, sigma: Array_Function) -> ndarray: ''' Вычисление частной производной функции по первому аргументу. ''' assert X.shape[1] == W.shape[0] # умножение матриц N = np.dot(X, W) # передача результата умножения матриц в функцию сигма S = sigma(N) # обратный проход dSdN = deriv(sigma, N) # вычисление dNdX dNdX = np.transpose(W, (1, 0)) # произведение результатов; так как dNdX имеет размерность 1x1, # порядок множителей не имеет значения return np.dot(dSdN, dNdX) Обратите внимание, что, как и в предыдущем примере, мы вычисляем значение функции во время прямого прохода (здесь оно обозначено просто как N), а затем используем это значение для обратного прохода. Проверка корректности результата Как определить, правильно ли мы нашли производные? Есть простой способ. Нужно немного поменять входное значение и посмотреть, как это отразится на результате. Например, в рассматриваемом случае мы увидим, что вектор X равен: print(X) [[ 0.4723 0.6151 -1.7262]] Вычислительный граф для двух матриц 47 Если увеличить х 3 на 0,01, то есть с –1,726 до –1,716, значение функции, которое мы вычисляем во время прямого прохода, тоже должно немного увеличиться, что мы и видим на рис. 1.20. Текущее значение Текущее значение + 0.01 Выходное значение x 3 Выход: новый Выход: исходный 0.01 Градиент = наклон в точке Разница должна быть близка к (исходное значение) + (градиент в точке) ₓ (0.01) Рис. 1.20. Проверка градиента А теперь выведем значение производной matrix_function_backward_1. Мы увидим, что наш градиент равен -0.1121 : print(matrix_function_backward_1(X, W, sigmoid)) [[ 0.0852 -0.0557 -0.1121]] Если мы правильно рассчитали градиент, увеличение переменной x 3 на 0,01 должно привести к уменьшению производной примерно на 0,01 × -0,1121 = -0,001121 . Любой другой результат, как в большую, так и в меньшую сторону, будет означать, что цепное правило не работает. В данном случае вычисления 1 показывают, что мы все посчитали верно! В заключение рассмотрим пример, в основе которого лежит весь ранее изученный материл. Этот пример непосредственно касается моделей, которые мы будем создавать в следующей главе. Вычислительный граф для двух матриц Как в машинном обучении в целом, так и в глубоком обучении в част- ности на вход подаются два двумерных массива, один из которых пред- 1 Код для материалов этой главы вы найдете в репозитории GitHub oreil.ly/2ZUwKOZ 48 Глава 1. Математическая база ставляет набор данных X, а второй — их веса W. В следующей главе мы подробно поговорим о причинах такого представления данных, а пока сосредоточимся на математической стороне дела. В частности, мы по- кажем, что цепное правило работает даже после перехода от скалярного произведения векторов к произведению матриц и что написать код для вычисления такой производной по-прежнему очень просто. Математические расчеты, как и в предыдущих случаях, не сложные, но объемные. При этом они дают краткий и лаконичный результат. Разуме- ется, мы рассмотрим процесс пошагово и свяжем его с кодом и схемами. Математическое представление Пусть , а Это может быть набор данных, в котором каждому наблюдению сопо- ставлены три признака. Три строки соответствуют трем наблюдениям, для которых мы хотим получить некий прогноз. Проделаем с этими матрицами следующие операции: 1. Найдем их произведение N = ν(X, W). 2. Подадим выходные данные функции N на вход дифференцируемой функции σ, обозначив это как S = σ(N). Как и прежде, требуется найти градиенты S по аргументам X и W и опре- делить, можно ли в этом случае воспользоваться цепным правилом. Отличие от ранее рассмотренных случаев состоит в том, что теперь при- дется иметь дело с матрицами, а не с числами. Возникает вопрос, что такое градиент одной матрицы по другой? Вычислительный граф для двух матриц 49 Нам доступны различные действия с многомерными массивами, но чтобы корректно определить понятие «градиент» по выходным данным, нужно суммировать (или каким-то другим способом превратить в одно число) последний массив последовательности. Только тогда будет иметь смысл вопрос: «Насколько изменение каждого элемента матрицы X повлияет на конечный результат?» Поэтому мы добавим функцию лямбда, суммирующую элементы функ- ции S. Теперь опишем это языком формул. Начнем с произведения матриц X и W: Элемент результирующей матрицы, расположенный в ряду i и столбце j, для удобства обозначим XW ij Передадим полученную матрицу в функцию σ, что означает применение этой функции ко всем элементам произведения матриц X ×W: = После этого остается найти сумму всех элементов: 50 Глава 1. Математическая база Теперь все свелось к уже знакомой задаче из математического анализа: есть функция L и нужно найти ее градиент по переменным X и W, чтобы узнать, насколько на нее повлияет изменение каждого элемента входных матриц (x 11 , w 21 и т. д.). Математически это записывается следующим образом: Теперь давайте посмотрим, как наша задача описывается языком схем, и напишем соответствующий код. Визуализация Концептуально мы делаем то же, что и в предыдущих примерах с вы- числительными графами для многих переменных. Вы без труда сможете понять рис. 1.21. γ [x] [w] N S δ L Λ Рис. 1.21. Граф прямого прохода сложной функции Мы просто передаем данные в функцию и утверждаем, что и в этом случае цепное правило позволит вычислить нужные градиенты. Код Вот как может выглядеть наш код: def matrix_function_forward_sum(X: ndarray, W: ndarray, sigma: Array_Function) -> float: ''' Прямой проход функции объектов ndarray X и W и функции sigma. Самое интересное: обратный проход 51 ''' assert X.shape[1] == W.shape[0] # умножение матриц N = np.dot(X, W) # передача результата умножения матриц в функцию сигма S = sigma(N) # сумма всех элементов L = np.sum(S) return L Самое интересное: обратный проход Пришло время выполнить обратный проход и показать, каким образом, даже в случае умножения матриц, мы можем вычислить градиент N по каждому элементу входных объектов ndarray 1 . Как только вы поймете, как все происходит, то без проблем сможете перейти к обучению моделей, которым мы и займемся в главе 2. Первым делом вспомним, что нужно делать. Визуализация Выполняем уже знакомую по предыдущим примерам процедуру; рис. 1.22 должен быть вам знаком, как и рис. 1.21. γ [x] [w] N (N) S σ L Λ ∂ γ ∂ x ∂ γ ∂ w ∂ σ ∂ u (S) ∂ Λ ∂ u Рис. 1.22. Обратный проход через сложную функцию 1 Так как градиенты N по X и по W вычисляется одинаково, мы рассмотрим только градиент по X. 52 Глава 1. Математическая база Нужно вычислить частную производную каждого элемента функции, оце- нить ее по входным данным и перемножить результаты, чтобы получить окончательную производную. Давайте по очереди рассмотрим каждую из частных производных. Математическое представление Отимечу, что все вычисления можно произвести, что называется, в лоб. Ведь L — это функция переменных x 11 , x 12 и т. д., до переменной x 33 Но эта задача выглядит очень сложной. В конце концов, смысл цепного правила в том, что оно позволяет разбивать сложные функции на со- ставные части, производить вычисления с этими частями и перемножать результаты. Именно это дает возможность легко писать код вычисления производных: достаточно пошагово выполнить прямой проход, сохранить результат и использовать его для оценки нужных нам производных во время обратного прохода. Я покажу, что этот подход работает и в случае матриц. Обозначим функцию как L. Будь это обычная функция скалярных переменных, то в соответствии с цепным правилом можно было бы написать: После чего оставалось бы по очереди вычислить каждую из трех частных производных. Именно так мы поступали раньше в примере с тремя вло- женными функциями. Согласно рис. 1.22, этот подход должен сработать и сейчас. Вычислить последний компонент просто. Мы хотим узнать, насколько возрастет функция L при увеличении каждого элемента функции S. Как вы помните, L представляет собой сумму всех элементов функции S, и производная будет: , Самое интересное: обратный проход 53 так как увеличение любого элемента функции S, например, на 0.46 еди- ницы будет увеличивать Λ на те же 0.46 единицы. Дальше следует компонент . Это производная функции σ, оценен- ная для рассматриваемого значения элементов произведения матриц N. Перейдя к ранее использовавшемуся обозначению XW, получим: Ничто не мешает выполнить поэлементное умножение двух производных и вычислить : А дальше начинаются сложности. Ведь согласно схеме и цепному прави- лу, дальше нужно вычислить . Еще раз напомню, что N — это вывод функции ν, то есть результат умножения матриц X и W. Фактически мы хотим понять, насколько увеличение каждого элемента матрицы X (раз- мером 3 × 3) повлияет на каждый элемент матрицы N (размером 3 × 2). Не совсем понятно, каким образом выполнить такой переход между матрицами разной формы и будет ли это вообще иметь смысл. Если помните, раньше нам везло. Ведь матрица X оказывалась транспо- нированной матрицей W, и наоборот. И мы получали, что , |