Главная страница
Навигация по странице:

  • Ключевое ограничение: работа с ветвлениями

  • Реализация накопления градиента

  • Иллюстрация автоматического дифференцирования

  • Объясним, что случилось

  • Актуальность рекуррентных нейронных сетей

  • Рис. 6.2.

  • Введение в рекуррентные нейронные сети

  • Курсовая работа. Глубокое обучение


    Скачать 4.97 Mb.
    НазваниеГлубокое обучение
    АнкорКурсовая работа
    Дата26.06.2022
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаVeydman_S_Glubokoe_obuchenie_Legkaya_razrabotka_proektov_na_Pyth.pdf
    ТипДокументы
    #615357
    страница16 из 22
    1   ...   12   13   14   15   16   17   18   19   ...   22
    191
    в каждом элементе последовательности. Таким образом, на вход RNN будет подаваться трехмерный ndarray вида
    [batch_size,
    sequence_length,
    num_features]
    , т. е. пакет последовательностей.
    Второе: для работы с трехмерными входными данными нужно исполь- зовать новый тип архитектуры нейронных сетей, которому как раз и посвящена эта глава. Но именно с третьей модификации мы и начнем обзор. Для работы с такой новой формой данных придется использовать совершенно другую структуру и другие абстракции. Почему? И в полно- связных, и в сверточных нейронных сетях каждая «операция», даже если она фактически представляет собой множество отдельных сложений и умножений (как в случае умножения матриц или свертки), может быть описана как цельная «мини-фабрика», которая и на прямом, и на обратном проходе принимает на вход один ndarray и выдает также один ndarray на выходе (при это может использоваться еще один ndarray с параметрами операции для выполнения вычислений). Как оказалось, рекуррентные нейронные сети так реализовать не получится. Прежде чем мы разберем- ся, почему, подумайте вот над чем: какие характеристики архитектуры нейронной сети делают невозможным использование созданной нами платформы? Ответ станет для вас озарением, но для описания полного пути его получения потребовалось бы углубиться в детали реализации, которые выходят за рамки этой книги. Давайте рассмотрим ключевое ограничение платформы, которую использовали до сих пор.
    Ключевое ограничение: работа с ветвлениями
    Оказывается, наша структура не позволяет обучать модели с помощью вычислительных графов, показанных на рис. 6.1.
    L
    α
    A
    1
    W
    1
    B
    1
    Λ
    A
    2
    W
    2
    B
    2
    B
    2
    C
    1
    Λ
    M
    Рис. 6.1. Вычислительный граф, который приводит к сбою в классе Operation: поскольку во время прямого прохода обрабатывается много разных данных, это значит, что мы не можем отправлять
    «назад» градиенты во время обратного прохода, как раньше

    192
    Глава 6. Рекуррентные нейронные сети
    В чем проблема? Преобразование прямого прохода в код выглядит хорошо
    (обратите внимание:
    Add и
    Multiply приведены здесь исключительно для демонстрационных целей):
    a1 = torch.randn(3,3)
    w1 = torch.randn(3,3)
    a2 = torch.randn(3,3)
    w2 = torch.randn(3,3)
    w3 = torch.randn(3,3)
    # операции wm1 = WeightMultiply(w1)
    wm2 = WeightMultiply(w2)
    add2 = Add(2, 1)
    mult3 = Multiply(2, 1)
    b1 = wm1.forward(a1)
    b2 = wm2.forward(a2)
    c1 = add2.forward((b1, b2))
    L = mult3.forward((c1, b2))
    Проблемы начинаются на обратном проходе. Допустим, мы хотим ис- пользовать уже привычное цепное правило для вычисления производной от
    L
    по w1
    . Раньше мы просто обращались к каждой операции в обратном порядке. Здесь же, из-за повторного использования b2
    во время прямого
    прохода, этот подход не сработает. Например, если бы мы начали с об- ратного вызова на mult3
    , то у нас были бы градиенты для обоих входов c1
    и b2
    . А если бы затем мы вызвали функцию backward на add2
    , то не смогли бы подать на вход градиент для c1
    , потому что нужен еще и градиент для b2
    , так как он тоже влияет на значение потери
    L
    . Таким образом, для правильного выполнения обратного прохода уже не получится просто перемещаться по операциям в обратном порядке. Вместо этого придется написать что-то вроде следующего:
    c1_grad, b2_grad_1 = mult3.backward (L_grad)
    b1_grad, b2_grad_2 = add2.backward (c1_grad)
    # объединение градиентов, показывающее, что b2

    Ключевое ограничение: работа с ветвлениями
    193
    # используется на прямом проходе дважды b2_grad = b2_grad_1 + b2_grad_2
    a2_grad = wm2.backward (b2_grad)
    a1_grad = wm1.backward (b1_grad)
    На этом этапе можно полностью отказаться и от использования класса
    Operation
    . Вместо этого можно сохранить все величины, которые вы- числяются на прямом проходе, и повторно использовать их на обратном, как делалось в главе 2! Всегда можно реализовать сколь угодно сложные нейронные сети, вручную прописывая отдельные вычисления, которые необходимо выполнить на прямом и обратном проходах. В главе 2 мы так уже делали, когда записывали 17 отдельных операций для обратного прохода двухслойной нейронной сети (и что-то подобное мы сделаем в этой главе внутри «ячеек RNN»). Класс
    Operation мы вводили с целью создать гибкую структуру, которая позволит на высоком уровне описать нейронную сеть и сделать так, чтобы все вычисления низкого уровня «про- сто работали». Такая структура хорошо иллюстрирует многие ключевые понятия нейронных сетей, но сейчас мы увидели ее недостатки.
    У этой проблемы есть элегантное решение: автоматическое дифферен- цирование. Это совершенно иной способ реализации нейронных сетей
    1
    Мы рассмотрим эту концепцию в достаточной степени, чтобы понять, как она работает, но слишком углубляться не станем, так как созданию полнофункциональной среды автоматического дифференцирования пришлось бы посвятить несколько глав. Кроме того, в следующей главе, посвященной PyTorch, мы рассмотрим использование высокопроизво- дительного фреймворка автоматического дифференцирования. Сама концепция автоматического дифференцирования довольно важна, и нам надо поговорить о ней, прежде чем мы перейдем к RNN. Мы разработаем базовую структуру для этого механизма и покажем, как она помогает ре- шить проблему с повторным использованием объектов во время прямого прохода, о которой мы говорили ранее.
    1
    Стоит упомянуть альтернативное решение этой проблемы, которым автор Даниэль
    Сабинас поделился в своем блоге: он представляет операции в виде графа, а затем использует поиск по ширине для вычисления градиентов на обратном проходе в правильном порядке. В результате получается структура, похожая на TensorFlow.
    В его постах в блоге все это рассказывается логично и понятно.

    194
    Глава 6. Рекуррентные нейронные сети
    Автоматическое дифференцирование
    Как мы уже видели, существуют архитектуры нейронных сетей, в которых наш класс
    Operation не позволяет с легкостью вычислить градиенты вы- ходных данных относительно входных данных, а именно это нам нужно для обучения моделей. Автоматическое дифференцирование позволяет вычислять эти градиенты совершенно иначе: вместо того чтобы состав- лять сеть из классов
    Operation
    , мы определяем класс, который оборачи- вается вокруг самих данных и позволяет данным отслеживать операции, выполняемые над ними. В результате данные смогут постоянно накап- ливать градиенты по мере прохождения через операции. Чтобы лучше понять, как будет работать это «накопление градиента», напишем код
    1
    Реализация накопления градиента
    Чтобы автоматически отслеживать градиенты, мы должны переписать методы Python, которые выполняют основные операции над данными.
    В Python использование операторов
    +
    или

    фактически вызывает скры- тые методы
    __add__
    и
    __sub__
    . Например, вот как это работает с оператором сложения:
    a = array([3,3])
    print("Addition using '__add__':", a.__add__(4))
    print("Addition using '+':", a + 4)
    Addition using '__add__': [7 7]
    Addition using '+': [7 7]
    Благодаря этому мы можем написать класс, который оборачивается во- круг типичного «числа» Python (
    float или int
    ) и перезаписывает методы add и mul
    :
    Numberable = Union[float, int]
    def ensure_number(num: Numberable) -> NumberWithGrad:
    if isinstance(num, NumberWithGrad):
    1
    Для более глубокого понимания того, как внедрить автоматическое дифференци- рование, почитайте книгу Grokking Deep Learning Эндрю Траска (Manning) (На русском: Траск Э. Грокаем глубокое обучение. — СПб.: Питер, 2020. — 352 с.).

    Автоматическое дифференцирование
    195
    return num else:
    return NumberWithGrad(num)
    class NumberWithGrad(object):
    def __init__(self, num: Numberable, depends_on: List[Numberable] = None, creation_op: str = ''):
    self.num = num self.grad = None self.depends_on = depends_on or []
    self.creation_op = creation_op def __add__(self, other: Numberable) -> NumberWithGrad:
    return NumberWithGrad(self.num + ensure_number(other).num, depends_on = [self, ensure_number(other)], creation_op = 'add')
    def __mul__(self, other: Numberable = None) -> NumberWithGrad:
    return NumberWithGrad(self.num * ensure_number(other).num, depends_on = [self, _number(other)], creation_op = 'mul')
    def backward(self, backward_grad: Numberable = None) -> None:
    if backward_grad is None: # first time calling backward self.grad = 1
    else:
    # В этих строках реализовано накопление градиентов.
    # Если градиент пока не существует, он становится равен
    # backward_grad if self.grad is None:
    self.grad = backward_grad
    # В противном случае backward_grad добавляется
    # к существующему градиенту else:
    self.grad += backward_grad

    196
    Глава 6. Рекуррентные нейронные сети if self.creation_op == "add":
    # Назад отправляется self.grad, так как увеличение
    # любого из этих элементов приведет к такому же
    # увеличению выходного значения self.depends_on[0].backward(self.grad)
    self.depends_on[1].backward(self.grad)
    if self.creation_op == "mul":
    # Расчет производной по первому элементу new = self.depends_on[1] * self.grad
    # Отправка производной по этому элементу назад self.depends_on[0].backward(new.num)
    # Расчет производной по второму элементу new = self.depends_on[0] * self.grad
    # Отправка производной по этому элементу назад self.depends_on[1].backward(new.num)
    В этом коде много чего происходит, поэтому давайте распакуем класс
    NumberWithGrad и посмотрим, как он работает. Напомним, что этот класс позволяет писать простые операции и автоматически рассчитывать гра- диенты. Например:
    a = NumberWithGrad(3)
    b = a * 4
    c = b + 5
    Как сильно увеличение на
    ϵ в данном случае увеличит значение c
    ? До- вольно очевидно, что увеличение получится в 4 раза. И действительно, если мы в этом классе сначала напишем:
    c.backward ()
    ,
    то затем можно просто написать вот так, не используя циклы for
    :
    print(a.grad)
    4
    Как это работает? Фундаментальный секрет описанного выше класса заключается в том, что каждый раз, когда над объектом
    NumberWithGrad выполняется операция
    +
    или
    *
    , создается новый объект
    NumberWithGrad
    ,

    Автоматическое дифференцирование
    197
    зависящий от
    NumberWithGrad
    . Затем, когда на объекте
    NumberWithGrad происходит обратный вызов, как ранее для c
    , все градиенты для всех объектов
    NumberWithGrad
    , использованных для создания c
    , вычисляются автоматически. И действительно, градиент в результате рассчитывается не только для а
    , но и для b
    :
    print(b.grad)
    1
    Но главное преимущество такой структуры заключается в том, что
    NumberWithGrads
    накапливают градиенты, что позволяет многократно использовать их во время серии вычислений, гарантированно получая правильный градиент. Мы покажем это на тех же операциях, которые ранее вызывали вопросы, но будем использовать
    NumberWithGrad
    , а затем подробно рассмотрим, как все работает.
    Иллюстрация автоматического дифференцирования
    В данной серии вычислений переменная a
    используется многократно:
    a = NumberWithGrad(3)
    b = a * 4
    c = b + 3
    d = c * (a + 2)
    Нетрудно посчитать, что после всего этого d
    =
    75
    , но вопрос заключается в другом: как сильно увеличится значение а
    при увеличении значения d
    ?
    Сначала мы можем найти ответ на этот вопрос математически. У нас есть:
    d = (4a + 3)
    × (a + 2) = 4a
    2
    + 11a + 6.
    Тогда, используя степенное правило:
    Следовательно, для a
    =
    3
    значение этой производной должно быть 8
    × 3 +
    + 11 = 35. Проверим это численно:
    def forward(num: int):
    b = num * 4
    c = b + 3

    198
    Глава 6. Рекуррентные нейронные сети return c * (num + 2)
    print(round(forward(3.01) — forward(2.99)) / 0.02), 3)
    35.0
    Теперь обратите внимание, что мы получим тот же результат, если будем вычислять градиент с помощью системы автоматического дифференци- рования:
    a = NumberWithGrad(3)
    b = a * 4
    c = b + 3
    d = (a + 2)
    e = c * d e.backward()
    print(a.grad)
    35
    Объясним, что случилось
    Мы увидели, что цель автоматического дифференцирования состоит в том, чтобы сделать фундаментальными единицами анализа сами объекты дан-
    ных — числа, объекты ndarray
    ,
    Tensor и т. д. — а не
    Operation
    , как было раньше.
    У всех методов автоматического дифференцирования есть общие черты:
    y
    У каждого метода есть класс, который оборачивается вокруг факти- чески вычисляемых данных. Здесь оборачивается
    NumberWithGrad во- круг чисел с плавающей точкой и целых чисел. К примеру, в PyTorch аналогичный класс называется
    Tensor y
    Общие операции, такие как сложение, умножение и умножение ма- триц, переопределяются так, чтобы они всегда возвращали экземпляр этого класса. В предыдущем случае мы реализовали
    NumberWithGrad и
    NumberWithGrad или
    NumberWithGrad с float или int y
    Класс
    NumberWithGrad должен содержать информацию о том, как вы- числять градиенты, с учетом информации о том, что происходит на прямом проходе. Ранее мы делали это путем включения в класс ар- гумента creation_op
    , в котором записывалось, как был создан класс
    NumberWithGrad

    Актуальность рекуррентных нейронных сетей
    199
    y
    На обратном проходе градиенты передаются в обратном направлении с использованием базового типа данных, а не «обертки». В нашем случае это означает, что градиенты будут иметь тип float и int
    , а не
    NumberWithGrad y
    Как мы говорили в начале этого раздела, автоматическое дифферен- цирование позволяет повторно использовать значения, вычисленные на прямом проходе, — в предыдущем примере мы без проблем дважды использовали значение a
    . Секрет заключается в этих строках:
    if self.grad is None:
    self.grad = backward_grad else:
    self.grad += backward_grad
    Здесь говорится, что после получения нового градиента, backward_
    grad
    , объект
    NumberWithGrad должен либо использовать в качестве этого значения градиент
    NumberWithGrad
    , либо просто добавить его значение к существующему градиенту
    NumberWithGrad
    . Это позволяет
    NumberWithGrad накапливать градиенты, когда в модели повторно ис- пользуются соответствующие объекты.
    На этом с автоматическим дифференцированием закончим. Давайте теперь обратимся к структуре модели, ради которой затеяли все эти объяснения, поскольку для вычисления прогноза требуется повторное использование некоторых значений во время прямого прохода.
    Актуальность рекуррентных нейронных сетей
    Как говорилось в начале этой главы, рекуррентные нейронные сети предназначены для обработки данных, представленных в виде последо- вательностей: каждое наблюдение теперь представляет собой не вектор с n
    объектами, а двумерный массив размерности n
    объектов на t
    времен- ных шагов (рис. 6.2).
    В следующих нескольких разделах я расскажу, как в RNN содержатся такие данные, но сначала давайте попытаемся понять, зачем они нуж- ны. Почему обычные нейронные сети с прямой связью не подходят для обработки таких данных? Можно представить каждый временной шаг в виде независимого набора признаков. Например, одно наблюдение может содержать признаки, соответствующие времени t = 1
    , а целевым

    200
    Глава 6. Рекуррентные нейронные сети станет набор t = 2
    . Затем следующее наблюдение может иметь признаки от времени t = 2
    , а целевым станет набор t = 3
    , и т. д. Если бы мы хотели для составления прогнозов использовать данные нескольких временных шагов, а не одного временного шага, то могли бы использовать функции от t = 1
    и t = 2
    , чтобы прогнозировать целевое значение t = 3
    , а от t = 2
    и t = 3
    , чтобы прогнозировать целевое значение при t = 4
    и т. д.
    Однако обработка каждого временного шага как независимого набора не учитывает тот факт, что данные расположены в определенной по- следовательности. Как учесть последовательный характер данных для составления более точных прогнозов? Решение будет примерно таким:
    1. Использовать признаки от временного шага t = 1
    , чтобы составить прогнозы для соответствующей цели при t = 1 2. Использовать признаки от временного шага t = 2
    , а также инфор-
    мацию от
    t = 1
    , включая значение цели при
    t = 1
    , чтобы составить прогнозы для t = 2 3. Использовать признаки от t = 3
    , а также накопленную информацию
    с
    t = 1
    и
    t = 2
    , чтобы составить прогнозы при t = 3 4. Далее на каждом шаге использовать информацию от всех предыду- щих шагов, чтобы составить прогноз.
    Чтобы сделать это, нам придется передавать наши данные через нейрон- ную сеть по одному элементу последовательности за раз, причем сначала должны использоваться данные с первого временного шага, затем со следующего временного шага и т. д. Кроме того, мы хотим, чтобы по мере обработки новых элементов последовательности наша нейронная сеть «на- капливала» информацию о том, что видела раньше. В оставшейся части этой главы будет подробно рассмотрена реализация этого в рекуррентных нейронных сетях. Существует несколько вариантов реализации рекур-
    f
    11
    f
    1+
    n признаков
    t = число временных шагов
    Время
    Цель
    f
    n1
    f
    n+
    t
    2
    t
    t+1
    Рис. 6.2. Последовательности данных: на каждом из t временных шагов у нас есть n признаков

    Введение в рекуррентные нейронные сети
    201
    рентных нейронных сетей, но у всех них есть общая базовая структура последовательной обработки данных. Сначала обсудим эту структуру, а затем рассмотрим, чем отличаются варианты.
    Введение в рекуррентные нейронные сети
    Начнем с того, что на высоком уровне обсудим, как передаются данные через «прямую» нейронную сеть. В таком типе сети данные передаются через несколько слоев. Для одного наблюдения результат работы — это
    «представление» наблюдения на этом слое. После первого слоя это пред- ставление состоит из признаков, которые являются комбинациями ис- ходных признаков. После следующего слоя оно состоит из комбинаций этих представлений или «признаков признаков» исходных элементов и т. д. для последующих слоев сети. Таким образом, после каждого пря- мого прохода сеть будет на выходах каждого из своих слоев содержать множество представлений исходного наблюдения (рис. 6.3).
    Вход: вектор числовых признаков
    Вектор
    «признаков признаков»
    Вектор конечного представления наблюдения сетью
    W
    1
    W
    2
    W
    n
    W
    n+1
    Вектор прогноза
    Обычная нейросеть
    1   ...   12   13   14   15   16   17   18   19   ...   22


    написать администратору сайта