Изучите более 1,5 млн электронных книг и аудиокниг бесплатно в течение дней

От $11.99 в месяц после пробного периода. Можно отменить в любое время.

Глубокое обучение: легкая разработка проектов на Python
Глубокое обучение: легкая разработка проектов на Python
Глубокое обучение: легкая разработка проектов на Python
Электронная книга446 страниц2 часа

Глубокое обучение: легкая разработка проектов на Python

Рейтинг: 0 из 5 звезд

()

Читать отрывок

Об этой электронной книге

Взрывной интерес к нейронным сетям и искусственному интеллекту затронул уже все области жизни, и понимание принципов глубокого обучения необходимо каждому разработчику ПО для решения прикладных задач.
Эта практическая книга представляет собой вводный курс для всех, кто занимается обработкой данных, а также для разработчиков ПО. Вы начнете с основ глубокого обучения и быстро перейдете к более сложным архитектурам, создавая проекты с нуля. Вы научитесь использовать многослойные, сверточные и рекуррентные нейронные сети. Только понимая принцип их работы (от «математики» до концепций), вы сделаете свои проекты успешными.
В этой книге:
- Четкие схемы, помогающие разобраться в нейросетях, и примеры рабочего кода.
- Методы реализации многослойных сетей с нуля на базе простой объектно-ориентированной структуры.
- Примеры и доступные объяснения сверточных и рекуррентных нейронных сетей.
- Реализация концепций нейросетей с помощью популярного фреймворка PyTorch.
ЯзыкРусский
ИздательПитер
Дата выпуска1 окт. 2024 г.
ISBN9785446116751
Глубокое обучение: легкая разработка проектов на Python

Связано с Глубокое обучение

Похожие электронные книги

«Интеллект (искусственный) и семантика» для вас

Показать больше

Отзывы о Глубокое обучение

Рейтинг: 0 из 5 звезд
0 оценок

0 оценок0 отзывов

Ваше мнение?

Нажмите, чтобы оценить

Отзыв должен содержать не менее 10 слов

    Предварительный просмотр книги

    Глубокое обучение - Сет Вейдман

    Глава 1. Математическая база

    Не нужно запоминать эти формулы. Если вы поймете принципы, по которым они строятся, то сможете придумать собственную систему обозначений.

    Джон Кохран, методическое пособие Investments Notes, 2006

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

    Каждую концепцию мы будем рассматривать с трех сторон:

    • математическое представление в виде формулы или набора уравнений;

    • код, по возможности содержащий минимальное количество дополнительного синтаксиса (для этой цели идеально подходит язык Python);

    • рисунок или схема, иллюстрирующие происходящий процесс.

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

    И начнем мы с такой простой, но очень важной математической концепции, как функция.

    Функции

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

    Математическое представление

    Вот два примера функций в математической форме записи:

    • .

    • .

    Записи означают, что функция f1 преобразует входное значение x в x², а функция f2 возвращает наибольшее значение из набора (x, 0).

    Визуализация

    Вот еще один способ представления функций:

    1. Нарисовать плоскость xy (где x соответствует горизонтальной оси, а y — вертикальной).

    2. Нарисовать на этой плоскости набор точек, x-координаты которых (обычно равномерно распределенные) соответствуют входным значениям функции, а y-координаты — ее выходным значениям.

    3. Соединить эти точки друг с другом.

    Французский философ и математик Рене Декарт первым использовал подобное представление, и его начали активно применять во многих областях математики, в частности в математическом анализе. Пример графиков функций показан на рис. 1.1.

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

    Рис. 1.1. Две непрерывные дифференцируемые функции

    Рис. 1.2. Другой способ представления тех же функций

    Код

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

    Примечание № 1. NumPy

    Библиотека NumPy для Python содержит реализации вычислительных алгоритмов, по большей части написанные на языке C и оптимизированные для работы с многомерными массивами. Данные, с которыми работают нейронные сети, всегда хранятся в многомерных массивах, чаще всего в дву- или трехмерных. Объекты ndarray из библиотеки NumPy дают возможность интуитивно и быстро работать с этими массивами. Например, если сохранить данные в виде обычного или многомерного списка, обычный синтаксис языка Python не позволит выполнить поэлементное сложение или умножение списков, зато эти операции прекрасно реализуются с помощью объектов ndarray:

    print(операции со списками на языке Python:)

    a = [1,2,3]

    b = [4,5,6]

    print(a+b:, a+b)

    try:

        print(a*b)

    except TypeError:

        print(a*b не имеет смысла для списков в языке Python)

    print()

    print(операции с массивами из библиотеки numpy:)

    a = np.array([1,2,3])

    b = np.array([4,5,6])

    print(a+b:, a+b)

    print(a*b:, a*b)

    операции со списками на языке Python:

    a+b: [1, 2, 3, 4, 5, 6]

    a*b не имеет смысла для списков в языке Python

    операции с массивами из библиотеки numpy:

    a+b: [5 7 9]

    a*b: [ 4 10 18]

    Объект ndarray обладает и таким важным для работы с многомерными массивами атрибутом, как количество измерений. Измерения еще называют осями. Их нумерация начинается с 0, соответственно первая ось будет иметь индекс 0, вторая — 1 и т.д. В частном случае двумерного массива нулевую ось можно сопоставить строкам, а первую — столбцам, как показано на рис. 1.3.

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

    print('a:')

    print(a)

    print('a.sum(axis=0):', a.sum(axis=0))

    print('a.sum(axis=1):', a.sum(axis=1))

    a:

    [[1 2]

    [3 4]]

    a.sum(axis=0): [4 6]

    a.sum(axis=1): [3 7]

    Рис. 1.3. Двумерный массив из библиотеки NumPy, в котором ось с индексом 0 соответствует строкам, а ось с индексом 1 — столбцам

    Наконец, объект ndarray поддерживает такую операцию, как сложение с одномерным массивом. Например, к двумерному массиву a, состоящему из R строк и C столбцов, можно прибавить одномерный массив b длиной C, и библиотека NumPy выполнит сложение для элементов каждой строки массива a⁴:

    a = np.array([[1,2,3],

                  [4,5,6]])

    b = np.array([10,20,30])

    print(a+b:\n, a+b)

    a+b:

    [[11 22 33]

    [14 25 36]]

    Примечание № 2. Функции с аннотациями типов

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

    def __init__(self,

                 layers: List[Layer],     

                 loss: Loss, learning_rate: float = 0.01) -> None:

    Такое определение сразу дает представление о назначении класса. Вот для сравнения функция operation:

    def operation(x1, x2):

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

    def operation(x1: ndarray, x2: ndarray) -> ndarray:

    Сразу понятно, что функция берет два объекта ndarray, вероятно, каким-то способом комбинирует и выводит результат этой комбинации. В дальнейшем мы будем снабжать аннотациями типов все определения функций.

    Простые функции в библиотеке NumPy

    Теперь мы готовы написать код определенных нами функций средствами библиотеки NumPy:

    def square(x: ndarray) -> ndarray:

        '''

        Возведение в квадрат каждого элемента объекта ndarray.

        '''

        return np.power(x, 2)

    def leaky_relu(x: ndarray) -> ndarray:

        '''

        Применение функции Leaky ReLU к каждому элементу ndarray.

        '''

        return np.maximum(0.2 * x, x)

    Библиотека NumPy позволяет применять многие функции к объектам ndarray двумя способами: np.function_name(ndarray) или ndarray.function_name. Например, функцию relu можно было написать как x.clip(min = 0). В дальнейшем мы будем пользоваться записью вида np.function_name(ndarray). И даже когда альтернативная запись короче, как, например, в случае транспонирования двумерного объекта ndarray, мы будем писать не ndarray.T, а np.transpose(ndarray,(1,0)).

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

    Производные

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

    Математическое представление

    Математически производная определяется как предел отношения приращения функции к приращению ее аргумента при стремлении приращения аргумента к нулю:

    .

    Можно численно оценить этот предел, присвоив переменной Δ маленькое значение, например 0.001:

    .

    Теперь посмотрим на графическое представление нашей производной.

    Визуализация

    Начнем с общеизвестного способа: если нарисовать касательную к декартову представлению функции f, производная функции в точке касания будет равна угловому коэффициенту касательной. Вычислить этот коэффициент, или тангенс угла наклона прямой, можно, взяв разность значений функции f при a – 0.001 и a + 0.001 и поделив на величину приращения, как показано на рис. 1.4.

    Рис. 1.4. Производная как угловой коэффициент

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

    Рис. 1.5. Альтернативный способ визуализации концепции производной

    Со временем вы увидите, что для понимания глубокого обучения второе представление оказывается важнее первого.

    Код

    И наконец, код для вычисления приблизительного значения производной:

    from typing import Callable

    def deriv(func: Callable[[ndarray], ndarray], input_: ndarray,

                            delta: float = 0.001) -> ndarray:

        '''

        Вычисление производной функции func в каждом элементе массива

        input_.

        '''

        return (func(input_ + delta) — func(input_ — delta)) / (2 * delta)

    Выражение «P — это функция E» (я намеренно использую тут случайные символы) означает, что некая функция f берет объекты E и превращает в объекты P, как показано на рисунке. Другими словами, P — это результат применения функции f к объектам E:

    А вот так выглядит соответствующий код:

    def f(input_: ndarray) -> ndarray:

        # Какое-то преобразование

        return output

    P = f(E)

    Вложенные функции

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

    Визуализация

    Наглядно представить концепцию вложенной функции можно с помощью рисунка.

    На рис. 1.6 мы видим, что данные подаются в первую функцию, преобразуются, выводятся и становятся входными данными для второй функции, которая и дает окончательный результат.

    Рис. 1.6. Вложенные функции

    Математическое представление

    В математической нотации вложенная функция выглядит так:

    .

    Такое представление уже сложно назвать интуитивно понятным, потому что читать эту запись нужно не по порядку, а изнутри наружу. Хотя, казалось бы, это должно читаться как «функция f2 функции f1 переменной x», но на самом деле мы вычисляем f1 от переменной x, а затем — f2 от полученного результата.

    Код

    Чтобы представить вложенные функции в виде кода, для них первым делом нужно определить тип данных:

    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.

        '''

        assert len(chain) == 2, \

        Длина объекта 'chain' должна быть равна 2

        f1 = chain[0]

        f2 = chain[1]

        return f2(f1(x))

    Еще одна визуализация

    Так как составная функция, по сути, представляет собой один объект, ее можно представить в виде f1 f2, как показано на рис. 1.7.

    Рис. 1.7. Альтернативное представление вложенных функций

    Из математического анализа известно, что если все функции, из которых состоит составная функция, дифференцируемы в рассматриваемой точке, то и составная функция, как правило, дифференцируема в этой точке! То есть функция f1 f2 это просто обычная функция, от которой можно взять производную, — а именно производные составных функций лежат в основе моделей глубокого обучения.

    Для вычисления производных сложных функций нам потребуется формула, чем мы и займемся далее.

    Цепное правило

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

    Математическое представление

    В математической нотации теорема утверждает, что для значения x

    ,

    где u — вспомогательная переменная, представляющая собой входное значение функции.

    Производную функции f одной переменной можно обозначить как . Вспомогательная переменная в данном случае может быть любой, ведь запись и означает одно и то же. Я обозначил эту переменную u.

    Но позднее нам придется иметь дело с функциями нескольких переменных, например x и y. И в этом случае между и уже будет принципиальная разница.

    Именно поэтому в начале раздела мы записали производные с помощью вспомогательной переменной u и будем в дальнейшем использовать ее для производных функций одной переменной.

    Визуализация

    Формула из предыдущего раздела не слишком помогает понять суть цепного правила. Давайте посмотрим на рис. 8, иллюстрирующий, что же такое производная в простом случае f1 f2.

    Из рисунка интуитивно понятно, что производная составной функции должна представлять собой произведение производных входящих в нее функций. Предположим, что производная первой функции при u = 5 дает значение 3, то есть .

    Затем предположим, что значение первой функции при величине входного параметра 5 равно 1, то есть . Производную этой функции при u = 1 приравняем к –2, то есть .

    Рис. 1.8. Цепное правило

    Теперь вспомним, что эти функции связаны друг с другом. Соответственно если при изменении входного значения второго черного ящика на 1 мы получим на выходе значение –2, то изменение входного значения до 3 даст нам изменение выходного значения на величину –2 3 = –6. Именно поэтому в формуле для цепного правила фигурирует произведение:

    .

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

    Код

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

    def sigmoid(x: ndarray) -> ndarray:

        '''

        Применение сигмоидной функции к каждому элементу объекта ndarray.

        '''

        return 1 / (1 + np.exp(-x))

    А этот код использует цепное правило:

    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)

    Рис. 1.9. Результат применения цепного правила. (См. иллюстрацию в цвете: https://storage.piter.com/upload/new_folder/978544611675/0109.png)

    Кажется, цепное правило работает. Там, где функция наклонена вверх, ее производная положительна, там, где она параллельна оси абсцисс, производная равна нулю; при наклоне функции вниз ее производная отрицательна.

    Как математически, так и с помощью кода мы можем вычислять производную «составных» функций, таких как f1 f2, если обе эти функции дифференцируемы.

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

    Более длинная цепочка

    Возьмем три дифференцируемых функции f1, f2 и f3 и попробуем вычислить производную f1 f2 f3. Мы уже знаем, что функция, составленная из любого конечного числа дифференцируемых функций, тоже дифференцируема.

    Математическое представление

    Дифференцирование происходит по следующей формуле:

    .

    Здесь работает та же схема, что и в случае цепочки из двух функций , но формула не позволяет интуитивно понять, что именно происходит!

    Визуализация

    Лучшее представление о том, как работает эта формула, нам даст рис. 1.10.

    Рис. 1.10. Вычисление производной трех вложенных функций

    Ход рассуждений в данном случае будет таким же, как и ранее. Представим, что все три функции как бы нанизаны на струну, входное значение функции f1 f2 f3 обозначим a, а выходное — b. При изменении a на небольшое значение Δ результат f1(a) изменится на , умноженное на Δ. Следующий шаг в цепочке — функция f2(f1(x)) изменится на , умноженное на Δ. Аналогичным образом

    Нравится краткая версия?
    Страница 1 из 1