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

  • Рис. 6.9.

  • GRUNodes: код

  • LSTMNode: визуализация

  • Представление данных для языковой модели на основе RNN на уровне символов

  • Другие задачи моделирования языка

  • Объединение вариантов RNNLayer

  • Соединяем все вместе

  • Библиотека PyTorch

  • Класс PyTorch Tensor

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


    Скачать 4.97 Mb.
    НазваниеГлубокое обучение
    АнкорКурсовая работа
    Дата26.06.2022
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаVeydman_S_Glubokoe_obuchenie_Legkaya_razrabotka_proektov_na_Pyth.pdf
    ТипДокументы
    #615357
    страница19 из 22
    1   ...   14   15   16   17   18   19   20   21   22
    221
    шлюзы (которые на самом деле являются группами из трех операций) показаны как один блок. Но все же мы знаем, как осуществлять обратное распространение через
    Operation
    , из которых состоит каждый шлюз, по- этому сейчас будем придерживаться терминов шлюзов.
    Контакт
    Шлюз сброса
    R [0, 1]
    x
    H_in [–1, 1]
    U [0,1]
    x
    H_in [

    1,1] x
    (1 – U) [0, 1] x
    H_new [

    1,1]
    Шлюз обновления
    Выходной шлюз
    X_in
    Z
    R [0, 1]
    H_in
    [

    1, 1]
    H_new [–1, 1]
    U[0, 1]
    Шлюз забывания
    =
    H_out [–1,1]
    X_out
    Рис. 6.8. Передача потока данных через GRUNode и его шлюзы, вычисление X_out и H_out
    Шлюз обновления
    Выходной шлюз
    Контакт
    X_in
    Z
    H_in
    [-1, 1]
    X_out
    H_out [-1, 1]
    Рис. 6.9. Поток данных пропускается через RNNNode, проходя всего через два шлюза и создавая
    X_out и H_out

    222
    Глава 6. Рекуррентные нейронные сети
    Действительно, на рис. 6.9 показано представление классического
    RNNNode с использованием шлюзов.
    Можно представлять
    Operation
    , которые мы описали ранее, как пропуск входа и скрытого состояния через два шлюза.
    GRUNodes: код
    В следующем коде реализован прямой проход через
    GRUNode
    , как описано ранее:
    def forward(self,
    X_in: ndarray,
    H_in: ndarray, params_dict: Dict[str, Dict[str, ndarray]]) ->
    Tuple[ndarray]:
    '''
    param X_in: размер numpy формы (batch_size, vocab_size)
    param H_in: размер numpy формы (batch_size, hidden_size)
    return self.X_out: размер numpy формы (batch_size, vocab_size)
    return self.H_out: размер numpy формы (batch_size, hidden_size)
    '''
    self.X_in = X_in self.H_in = H_in
    # шлюз сброса self.X_r = np.dot(X_in, params_dict['W_xr']['value'])
    self.H_r = np.dot(H_in, params_dict['W_hr']['value'])
    # шлюз обновления self.X_u = np.dot(X_in, params_dict['W_xu']['value'])
    self.H_u = np.dot(H_in, params_dict['W_hu']['value'])
    # шлюзы self.r_int = self.X_r + self.H_r + params_dict['B_r']['value']
    self.r = sigmoid(self.r_int)
    self.u_int = self.X_r + self.H_r + params_dict['B_u']['value']
    self.u = sigmoid(self.u_int)
    # новое состояние self.h_reset = self.r * H_in self.X_h = np.dot(X_in, params_dict['W_xh']['value'])
    self.H_h = np.dot(self.h_reset, params_dict['W_hh']['value'])

    RNN: код
    223
    self.h_bar_int = self.X_h + self.H_h + params_dict['B_h']['value']
    self.h_bar = np.tanh(self.h_bar_int)
    self.H_out = self.u * self.H_in + (1 — self.u) * self.h_bar self.X_out = (
    np.dot(self.H_out, params_dict['W_v']['value']) \
    + params_dict['B_v']['value']
    )
    return self.X_out, self.H_out
    Обратите внимание, что мы явным образом не объединяем массивы
    X_in и
    H_in
    , поскольку в отличие от
    RNNNode
    , где они всегда используются вместе, здесь они используются по отдельности в
    GRUNodes
    ; в частности, мы ис- пользуем
    H_in независимо от
    X_in в строке self.h_reset = self.r * H_in
    Метод backward можно найти на сайте книги (
    https://oreil.ly/2P0lG1G
    ) — там реализован простой проход через операции, составляющие
    GRUNode
    , вычисляется производная каждой операции по отношению к ее входу, и результаты перемножаются.
    LSTMNodes
    Ячейки долгой краткосрочной памяти, или LSTM, являются наиболее популярной модификацией классических RNN. Причина заключается в том, что они были изобретены на заре глубокого обучения, еще в 1997 году
    1
    , а все альтернативы, в том числе GRU, появились в последние не- сколько лет (например, GRU были предложены в 2014 году).
    Как и GRU, LSTM дают RNN возможность «сбрасывать» или «забывать» свое скрытое состояние при получении новых входных данных. В GRU это достигается путем передачи входного и скрытого состояния через серию шлюзов, а также вычисления «потенциального» нового скрытого состояния с их помощью — self h_bar
    , вычисленного с помощью шлюза self r
    . После этого вычисляется окончательное скрытое состояние с использованием средневзвешенного значения предлагаемого нового скрытого состояния и старого скрытого состояния, что управляется шлюзом обновления:
    self.H_out = self.u * self.H_in + (1 — self.u) * self.h_bar
    1
    См. статью LSTM Long Short-Term Memory, автор Hochreiter и соавт. (1997).

    224
    Глава 6. Рекуррентные нейронные сети
    В LSTM, напротив, используется отдельный вектор «состояния», так на-
    зываемое «состояние ячейки», которым определяется, нужно ли «забыть»
    то, что находится в скрытом состоянии. Затем с помощью двух других шлюзов контролируется степень сброса или обновления состояния ячейки, а четвертый шлюз определяет степень обновления скрытого состояния, основываясь на последнем известном состоянии ячейки
    1
    LSTMNode: визуализация
    На рис. 6.10 показана схема
    LSTMNode с операциями, представленными в качестве шлюзов.
    Обновление состояния ячейки
    F [0, 1]
    x
    C_in [–1,1] x
    I [0, 1] x
    C_new [

    1,1]
    U [0, 1] x
    C_out [

    1,1]
    Шлюз обновления
    Шлюз обновления
    Входной шлюз
    Выходной шлюз
    C_new [

    1,1]
    Контакт
    X_in
    Z
    H_in
    [–1, 1]
    X_out
    U [0, 1]
    H_out [

    1,1]
    F [0, 1]
    = C_out [

    1,1]
    I [0, 1]
    Рис. 6.10. Поток данных передается через LSTMNode, проходя через серию шлюзов и выводя обновленные состояния ячеек и скрытые состояния C_out и H_out соответственно вместе с фактическим выходом X_out
    LSTM: код
    Как и в случае
    GRUNode
    , полный код для
    LSTMNode
    , включая метод backward и пример, показывающий, как эти узлы вписываются в
    LSTMLayer
    , при-
    1
    По крайней мере, стандартный вариант LSTM. Как уже упоминалось, есть и другие варианты, такие как «LSTM со смотровыми глазкˆами», шлюзы в которых располо- жены по-разному.

    RNN: код
    225
    веден на сайте книги (
    https://oreil.ly/2P0lG1G
    ). Здесь мы просто покажем метод forward
    :
    def forward(self,
    X_in: ndarray,
    H_in: ndarray,
    C_in: ndarray,
    params_dict: Dict[str, Dict[str, ndarray]]):
    '''
    param X_in: массив numpy формы (batch_size, vocab_size)
    param H_in: массив numpy формы (batch_size, hidden_size)
    param C_in: массив numpy формы (batch_size, hidden_size)
    return self.X_out: массив numpy формы (batch_size, output_size)
    return self.H: массив numpy формы (batch_size, hidden_size)
    return self.C: массив numpy формы (batch_size, hidden_size)
    '''
    self.X_in = X_in self.C_in = C_in self.Z = np.column_stack((X_in, H_in))
    self.f_int = (
    np.dot(self.Z, params_dict['W_f']['value']) \
    + params_dict['B_f']['value']
    )
    self.f = sigmoid(self.f_int)
    self.i_int = (
    np.dot(self.Z, params_dict['W_i']['value']) \
    + params_dict['B_i']['value']
    )
    self.i = sigmoid(self.i_int)
    self.C_bar_int = (
    np.dot(self.Z, params_dict['W_c']['value']) \
    + params_dict['B_c']['value']
    )
    self.C_bar = tanh(self.C_bar_int)
    self.C_out = self.f * C_in + self.i * self.C_bar self.o_int = (
    np.dot(self.Z, params_dict['W_o']['value']) \

    226
    Глава 6. Рекуррентные нейронные сети
    + params_dict['B_o']['value']
    )
    self.o = sigmoid(self.o_int)
    self.H_out = self.o * tanh(self.C_out)
    self.X_out = (
    np.dot(self.H_out, params_dict['W_v']['value']) \
    + params_dict['B_v']['value']
    )
    return self.X_out, self.H_out, self.C_out
    Это был последний компонент нашей инфраструктуры RNN, который понадобится для начала обучения моделей! Осталось рассмотреть еще один момент: как представить текстовые данные в форме, позволяющей передавать их в RNN.
    Представление данных для языковой модели на основе RNN
    на уровне символов
    Языковое моделирование — это одна из наиболее распространенных задач, для которых используются RNN. Как преобразовать последовательность символов в набор обучающих данных, чтобы RNN смогла предсказывать следующий символ? Самый простой способ — использовать горячую ко-
    дировку. Она работает следующим образом: каждая буква представляется в виде вектора размером, равным размеру словаря или количеству букв в общем наборе текста, по которому мы обучаем сеть (он рассчитывается заранее и жестко задан как гиперпараметр сети). Затем каждая буква представляется в виде вектора с 1 в позиции, соответствующей этой букве, и с 0 на других позициях. Наконец, векторы всех букв просто объ- единяются, формируя общее представление последовательности букв.
    Ниже приведен простой пример того, как это будет выглядеть со словарем из четырех букв a
    , b
    , c
    и d
    , где первой буквой будет a
    , второй — b
    и т. д.:

    RNN: код
    227
    Такой двумерный массив заменяет одно наблюдение формы
    (sequence_
    length,
    num_features)
    =
    (5,
    4)
    в серии последовательностей. Если бы у нас был текст abcdba длины 6 и мы хотели бы передать последовательности длиной 5 в наш массив, первая последовательность была бы преобразо- вана в предыдущую матрицу, а вторая последовательность имела бы вид:
    Затем они объединяются, формируя входные данные для
    RNN
    формы
    (batch_size,
    sequence_length,
    vocab_size)
    =
    (2,
    5,
    4)
    . Продолжая «склеи- вать» фрагменты, мы можем взять необработанный текст и преобразовать его в набор последовательностей для передачи в RNN.
    В разделе главы 6 в репозитории GitHub (
    https://oreil.ly/2P0lG1G
    ) это запи- сано в коде класса
    RNNTrainer
    , который может принимать необработанный текст, предварительно обрабатывать его, используя методы, описанные здесь, и подавать его в RNN партиями.
    Другие задачи моделирования языка
    Ранее мы этого не говорили, но, как видно из предыдущего кода, все варианты узлов
    RNNNode позволяют слоям
    RNNLayer выводить количество признаков, отличное от того, которое было на входе. Последний шаг у всех трех узлов — умножение конечного скрытого состояния сети на весовую матрицу, к которой мы получаем доступ через params_dict[W_v]
    Второе измерение этой весовой матрицы определяет размерность выво- да слоя. Это позволяет нам использовать одну и ту же архитектуру для разных задач моделирования языка, просто изменяя аргумент output_size в каждом слое.
    Например, мы только что рассмотрели построение языковой модели с помощью «предсказания следующего символа». В этом случае размер вывода будет равен размеру словаря: output_size
    =
    vocab_size
    . А, напри- мер, для сентимент-анализа, последовательности, которые мы передаем, могут просто иметь метку «0» или «1» — положительную или отрица- тельную. В этом случае мы получим output_size
    =
    1
    , а выходные данные

    228
    Глава 6. Рекуррентные нейронные сети будем сравнивать с целью только после того, как мы передадим всю по- следовательность. Схема работы показана на рис. 6.11.
    Ввод
    RNNLayer (с узлами)
    RNNLayer (с узлами)
    Output (batch_size, 1, 1)
    Рис. 6.11. Для сентимент-анализа RNN сравнивает прогноз с фактическими значениями и вырабатывает градиенты только для вывода последнего элемента последовательности; тогда обратное распространение будет продолжаться как обычно, при этом каждый из узлов, который не является последним, просто получит массив «X_grad_out» со всеми нулями
    Такая структура позволяет реализовать различные задачи моделирования языка. Если точнее, в ней можно выполнять любые задачи моделирова- ния, в которой данные являются последовательными и вводятся в сеть по одному элементу последовательности за раз.
    Прежде чем закончить, рассмотрим еще один редко обсуждаемый аспект
    RNN: как можно смешивать и совмещать разные типы слоев —
    GRULayer
    ,
    LSTMLayer и другие.
    Объединение вариантов RNNLayer
    Совместить различные типы
    RNNLayer очень просто: каждый слой
    RNN
    выводит ndarray формы
    (batch_size,
    sequence_length,
    output_size)
    , который можно передать на следующий слой. Как и в случае плотных слоев, указывать input_shape не нужно. Вместо этого мы просто задаем веса, основываясь на первом массиве, который слой получает в качестве входных данных, чтобы получить соответствующую форму с учетом вход- ных данных. Таким образом,
    RNNModel может иметь атрибут self.layers
    :

    RNN: код
    229
    [RNNLayer (hidden_size = 256, output_size = 128),
    RNNLayer (hidden_size = 256, output_size = 62)]
    Как и в случае с полносвязными нейронными сетями, мы просто должны быть уверены, что последний уровень производит вывод желаемой раз- мерности; здесь, если мы имеем дело со словарем размера 62 и выполняем предсказание следующего символа, наш последний уровень должен иметь размер_раздела 62
    , так же как последний уровень в наших полностью подключенных нейронных сетях, имеющих дело с проблемой MNIST, должен иметь измерение 10.
    После прочтения этой главы должно быть ясно (хотя это нечасто рас- сматривается при обработке RNN), что поскольку каждый вид слоя, который мы видели, имеет одинаковую базовую структуру, состоящую из последовательностей измерения feature_size и вывода последовательно- стей измерения output_size
    , то можно легко сложить разные виды слоев.
    Например, на веб-сайте книги (
    https://oreil.ly/2P0lG1G
    ) мы обучаем
    RNNModel с атрибутом self.layers
    :
    [GRULayer (hidden_size = 256, output_size = 128),
    LSTMLayer (hidden_size = 256, output_size = 62)]
    Иными словами, первый слой передает свои входные данные во времени с использованием
    GRUNode
    , а затем передает ndarray формы
    (batch_size,
    sequence_length,
    128)
    на следующий слой, который впоследствии про- пускает их через свои
    LSTMNode
    Соединяем все вместе
    Классическое упражнение для иллюстрации эффективности RNN — на- учить нейросеть писать текст в определенном стиле. На веб-сайте книги
    (
    https://oreil.ly/2P0lG1G
    ) мы выложили пример кода с моделью, опреде- ленной с использованием абстракций, описанных в этой главе. Модель обучается писать текст в стиле Шекспира. Единственный компонент, который мы не показали, — это класс
    RNNTrainer
    , который перебирает обучающие данные, предварительно обрабатывает их и передает их через модель. Основное различие между этим учителем и тем, что мы видели ранее, состоит в том, что в RNN при выборе пакета данных для передачи нужно сначала предварительно обработать его, закодировать каждую букву и объединить результирующие векторы в последовательность для

    230
    Глава 6. Рекуррентные нейронные сети преобразования каждой строки длиной sequence_length в ndarray формы
    (sequence_length,
    vocab_size)
    Но как только данные предварительно обработаны и модель определена,
    RNN обучается так же, как и другие знакомые нам нейронные сети: партии передаются одна за одной, прогнозы модели сравниваются с целями для расчета потерь, а затем значения потерь обратно распространяются через операции, которые составляют модель для обновления весов.
    Заключение
    В этой главе мы изучили рекуррентные нейронные сети — особый тип архитектуры нейронных сетей, предназначенный для обработки после- довательностей данных, а не отдельных операций. Вы узнали, что RNN состоят из слоев, которые передают данные во времени, обновляя свои скрытые состояния (и состояния их ячеек в случае LSTM) по мере работы.
    Мы узнали подробности о продвинутых вариантах RNN, GRU и LSTM и о том, как они передают данные через серию шлюзов на каждом вре- менном шаге. Также мы узнали, что последовательности данных во всех модификациях обрабатываются примерно одинаковым образом, общая структура одинаковая и отличается лишь конкретными операциями, которые применяются на каждом временном шаге.
    Надеемся, что теперь вы знаете чуть больше по этой сложной теме. В главе
    7 мы завершим повествование, перейдя к практической стороне глубо- кого обучения, а также покажем, как реализовать то, о чем мы говорили до сих пор, с помощью фреймворка PyTorch, высокопроизводительного, основанного на автоматическом дифференцировании фреймворка для построения и обучения моделей глубокого обучения. Вперед!

    ГЛАВА 7
    Библиотека PyTorch
    Из глав 5 и 6 вы узнали, как работают сверточные и рекуррентные ней- ронные сети, реализовав их с нуля. Понимание принципов, разумеется, необходимо, но одним лишь знанием нельзя заставить их работать над реальной проблемой. Теперь нам нужна возможность реализовать их в высокопроизводительной библиотеке. Можно было бы написать целую книгу по созданию высокопроизводительной библиотеки нейронных сетей, но это была бы совсем другая (или просто намного более длинная) книга для совсем другой аудитории. Вместо этого мы посвятим послед- нюю главу знакомству с PyTorch — набирающей популярность средой для нейронных сетей, основанной на автоматическом дифференцировании
    (мы упоминали ее в главе 6).
    Как и в остальной части книги, мы напишем наш код так, чтобы он соот- ветствовал принципам работы нейронных сетей, — напишем классы
    Layer
    ,
    Trainer и т. д. При этом мы не будем писать наш код в соответствии с рас- пространенными практиками PyTorch, но дадим ссылки на репозиторий
    GitHub (
    https://oreil.ly/2N4H8jz
    ), чтобы вы могли больше узнать о том, как писать нейронные сети так, как это задумано PyTorch. Но прежде давайте начнем с изучения типа данных в ядре PyTorch, которое реализует ав- томатическое дифференцирование и, следовательно, способность четко выражать обучение нейронной сети:
    Tensor
    Класс PyTorch Tensor
    В предыдущей главе мы показали, как класс
    NumberWithGrad накапливает градиенты, отслеживая операции, выполняемые над ним. Это означало, что если мы написали:
    a = NumberWithGrad(3)
    b = a * 4

    232
    Глава 7. Библиотека PyTorch c = b + 3
    d = (a + 2)
    e = c * d e.backward()
    тогда a
    grad будет равно
    35
    , что на самом деле является частной производ- ной от e
    по отношению к a
    Класс
    Tensor в PyTorch работает как «
    ndarrayWithGrad
    »: он похож на
    NumberWith
    Grad
    , за исключением использования массивов (например, numpy
    ) вместо простых чисел с плавающей точкой и целых чисел. Пере- пишем предыдущий пример с использованием класса
    Tensor
    . Сначала мы инициализируем
    Tensor вручную:
    a = torch.Tensor ([[3., 3.,],
    [3., 3.]], require_grad = True)
    Обратите внимание на пару вещей:
    y мы можем инициализировать класс Tensor, просто оборачивая данные, содержащиеся в нем, в torch.Tensor
    , как мы это делали с массивами ndarray
    ;
    y при такой инициализации Tensor мы должны передать аргумент require_grad = True
    , чтобы сказать Tensor накапливать градиенты.
    Как только мы это сделаем, мы можем выполнить вычисления, как и раньше:
    b = a * 4
    c = b + 3
    d = (a + 2)
    e = c * d e_sum = e.sum()
    e_sum.backward()
    По сравнению с примером
    NumberWithGrad здесь появился еще один шаг: мы должны суммировать e
    перед обратным вызовом на сумму. Мы уже говорили в первой главе, что не имеет смысла думать о «производной числа по массиву». Зато можно сказать, какой будет частная производ- ная e_sum по отношению к каждому элементу a
    — и действительно, мы видим, что ответ соответствует тому, что мы выяснили в предыдущих главах:

    Глубокое обучение с PyTorch
    1   ...   14   15   16   17   18   19   20   21   22


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