Пакетное обучение

Пакетное обучение простой рекуррентной нейронной сети

Здравствуйте и вновь добро пожаловать на занятия по теме «Рекуррентные нейронные сети на языке Python: глубокое обучение, часть 5».

В этой статье мы поговорим об одном способе улучшения писавшегося до сих пор кода.

Одно улучшение мы сделали ранее, когда ввели модульность. Если помните, в начале этого курса мы создали простую рекуррентную нейронную сеть, в которой рекуррентность была как бы встроена в нейронную сеть. Но затем мы начали говорить о нейронных сетях скорее как о наборе слоёв. Если вы проходили мои предыдущие курсы, то могли заметить, что в определённый момент мы уже создавали нейронные сети таким образом, чтобы они могли содержать любое число слоёв, а также различные виды этих слоёв. К примеру, когда мы работали со свёрточными нейронными сетями, то вначале имели произвольное количество слоёв свёртки и агрегирования, за которыми следовало произвольное количество полносвязных слоёв. Мы моделировали каждый из этих слоёв в виде класса. И в этом курсе мы сделали то же самое – мы создали слои GRU и LSTM в виде классов.

Ещё одно улучшение, которое обсуждалось ранее, но ещё не использовалось в этом курсе, – это пакетное обучение. Почему так? Ну, как вы могли заметить, материал и так был довольно сложным. Функция scan нетривиальна и имеет много составных частей, но, надеюсь, после многократного использования её различных возможностей вы уже с ней освоились.

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

Начнём с рассмотрения нашей первоначальной функции recurrence.

        def recurrence(x_t, h_t1):

            # returns h(t), y(t)

            h_t = self.f(x_t.dot(self.Wx) + h_t1.dot(self.Wh) + self.bh)

            y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)

            return h_t, y_t

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

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

Дело всё в той же причине, по которой мы прежде всего и хотим ввести пакетное обучение: как известно, одно большое умножение матриц происходит значительно быстрее, чем многократное умножение маленьких матриц в цикле. Поэтому операция умножения x(t)W с циклом по всем значениям t медленнее, чем простое умножение XW. Следовательно, на самом деле мы можем убрать эту часть из recurrence.

Что ещё?

Обратите внимание, та же ситуация и с y(t). y(t) зависит только от h(t) – скрытого состояния в один и тот же момент времени. Поэтому мы можем просто написать

Единственное место, где мы абсолютно никак не можем обойтись без рекурсии – это вычисление h(t), поскольку оно зависит от h(t-1).

Итак, взглянем на функцию recurrence после того, как уберём умножение на X и последнее умножение для получения исходящего y(t):

        XW = X.dot(Wx)

        def recurrence(xw_t, h_t1):

            h_t = self.f(xw_t + h_t1.dot(Wh) +  bh)

            return h_t

        h, _ = theano.scan(

            fn=recurrence,

            outputs_info=[h0],

            sequences=[XW],

            n_steps=XW.shape[0],

        )

    y = T.nnet.softmax(h.dot(Wo) + bo)

Она и в самом деле выглядит проще и короче, чем прежде.

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

Рассмотрим одно из возможных решений. Можно сделать X трёхмерным тензором с размерностью размер_пакета x длина_последовательности x количество_признаков, но всё равно останется цикл для каждого отдельного X. Получится рекурсия внутри рекурсии, что означает вложение функций scan. Но не забывайте, что scan почти так же плоха, как и цикл for, поэтому хотелось бы этого избежать.

Рассмотрим другое возможное решение. Предположим, мы сгладим наше X, так чтобы вместо размерности NxTxD он имел размерность N*TxD. Теперь оно двухмерное, и мы можем его целиком умножить на W. По крайней мере, первая часть задачи таким образом решена… Но вот можно ли это поместить в функцию recurrence?

Трудности возникают, когда мы пытаемся вычислить h(t). Когда мы «втискиваем» X в двухмерный массив, функция recurrence воспринимает его как одну большую последовательность, даже если это объединение нескольких последовательностей в пакете. Если найдётся способ «пояснить» h(t), что надо вновь начинать с h(0), а не с h(t-1) в начале каждой последовательности, то это не будет проблемой. Вопрос только в том, можно ли это сделать?

Конечно, можно. Но для этого потребуются знание работы Theano, которого у нас ещё нет. Что нам нужно – так это отслеживать, где начинается и где заканчивается каждая последовательность. Я опишу своё решение. Вероятно, это не единственное возможное решение, но именно это я придумал для данного курса.

Чтобы отслеживать, где начало и конец последовательности, мы введём новый массив размерности размер_пакета x длина_последовательности. Это будет массив индикаторов, состоящий из нулей, кроме тех случаев, когда начинается новая последовательность. То есть, если начинается последовательность – стоит 1, в противном случае 0.

Теперь у вас есть общее представление о решении, но как это реализовать? Для этого требуется больше сведений о Theano. Я дам вам минуту подумать, как это можно сделать, или вы даже можете остановить видео и попробовать сделать всё самостоятельно.

Итак, причина затруднения кажется непостижимой, но как только я вам расскажу, всё сразу станет очевидным. Суть в том, чтобы проверить эту последовательность индикаторов, чтобы решить, что делать дальше, – по сути, как условный оператор: если мы находимся в начале последовательности, выполнять первое выражение, в противном случае – второе:

if is_start:

    h(t) = f(xw_t + h0.dot(Wh) + bh)

else:

    h(t) = f(xw_t + h_t1.dot(Wh) + bh)

Это бы отлично работало в Numpy, но мы – в Theano. Проблема в том же, что и проблема невозможности использования в Theano цикла for – потому что ни одна из этих величин не имеет значений. Не забывайте, что сейчас мы создаём граф Theano, чтобы пояснить ему, что делать, когда появятся реальные числа. Поэтому что-либо вроде условного оператора, который пытается решить, что ему делать в момент своего вызова, работать не будет.

Но как функция scan в библиотеке Theano является заменителем цикла for, так и для условного оператора есть свой эквивалент, особенно если нужно что-то присвоить. Такой вещью является функция switch. Она принимает три аргумента. Первый – условное выражение, разумеется, состоящее из узлов графа Theano. Второй – что присвоить, если условие истинно, а третий аргумент – что присвоить, если условие ложно:

new_value = T.switch(

    condition,

    value_if_true,

    value_if_false

)

То есть, это просто эквивалент условного оператора if, когда возвращается первое значение, если условие истинное, и второе значение, если ложно:

Эквивалент вне Theano:

if condition:

     new_value = value_if_true

else:

     new_value = value_if_false

Использовав это обстоятельство, мы можем переписать нашу функцию recurrence с помощью функции scan:

def recurrence(xw_t, is_start, h_t1, h0):

    h_t = T.switch(

        T.eq(is_start, 1),

         self.f(xw_t + h0.dot(self.Wh) + self.bh),

         self.f(xw_t + h_t1.dot(self.Wh) + self.bh)

    )

    return h_t

# T.eq(a, b) в Theano является эквивалентом выражения a==b.

Обратите внимание, что T.eq(a, b) – это Theano-эквивалент равенства. Просто использовать == нельзя, поскольку оба аргумента не имеют значений.

А вот так мы вызываем функцию scan:

thStartPoints = T.ivector()

        h, _ = theano.scan(

            fn=recurrence,

            outputs_info=[self.h0],

            sequences=[XW, thStartPoints],

            non_sequences=[ self.h0],

            n_steps=XW.shape[0],

        )

При этом и XW, и thStartPoints являются входными последовательностями и оба имеют одинаковую длину.

Заметьте, что почти всё остальное в коде остаётся прежним. Главное отличие вне новой функции recurrence в том, что мы, конечно же, теперь должны делать цикл по пакетам. Мы также изменяем форму Xbatch и Ybatch перед вызовом train_op и создаём массив индикаторов startPoints:

for i in xrange(epochs):

    X, Y = shuffle(X, Y)

    N_correct = 0

    cost = 0

    for j in xrange(n_bathes):

        Xbatch = X[j*batch_sz:(j+1)* batch_sz.reshape(sequenceLength* batch_sz, D)

        Ybatch = Y[j*batch_sz:(j+1)* batch_sz.reshape(sequenceLength* batch_sz).astype(np.int32)

        c,p, rout = self.train_op(Xbatch, Ybatch, startPoints)

        …

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

startPoints = np.zeros(sequenceLength*batch_sz, dtype=np.int32)

for b in xrange(batch_sz):

    startPoints[b*aequenceLength] = 1

И последнее, что связано с кодом, – это оценка коэффициента классификации. Это становится небольшой проблемой, поскольку прогнозы теперь сглаживаются в одномерный массив. Можно сделать цикл с прохождением по числу пакетов с вычислением правильного  индекса, сигнализирующего о конце последовательности. Если выражение (b+1)*T даёт начало последовательности, то (b+1)*T – 1 будет означать конец предыдущей последовательности:

for b in xrange(batch_sz):

    idx = sequenceLength*(b + 1) – 1

    if p(idx) == Ybatch[idx]

        n_correct += 1

Остаётся только всё это запустить и удостовериться, что оно работает быстрее, чем первоначальный файл srn_parity.py. Если вы не хотите писать весь код самостоятельно, хотя я настоятельно рекомендую именно так и сделать, возьмите файл srn_parity.py и модифицируйте его, используя полученные в этой лекции сведения. Если же вы не хотите делать даже этого, то возьмите файл batch_parity.py. При этом обратите внимание, что я создал новую функцию all_parity_pairs_with_sequence_labels, которая предварительно подготавливает данные, преобразуя единичные целевые переменные в последовательности целевых переменных. Она находится в файле util.py. Об этом, вероятно, не сказано в первоначальной версии лекции, посвящённой коду для проблемы чётности, если я ещё её не обновил.

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: