Простой ванильный RNN не проходит проверку градиента - PullRequest
5 голосов
/ 30 апреля 2019

Я недавно пытался реализовать ваниль RNN с нуля.Я все реализовал и даже показал, казалось бы, хороший пример!все же я заметил, что проверка градиента не удалась!и только некоторые части (в частности, вес и смещение для выходных данных) проходят проверку градиента, в то время как другие веса (Whh, Whx) не проходят ее.

Я следил за реализацией karpathy / corsera и убедился, что все реализовано.И все же karpathy / код corsera проходит проверку градиента, а мой - нет.На данный момент я понятия не имею, что это за причина!

Вот фрагменты, отвечающие за обратный проход в исходном коде:

def rnn_step_backward(dy, gradients, parameters, x, a, a_prev):

    gradients['dWya'] += np.dot(dy, a.T)
    gradients['dby'] += dy
    da = np.dot(parameters['Wya'].T, dy) + gradients['da_next'] # backprop into h
    daraw = (1 - a * a) * da # backprop through tanh nonlinearity
    gradients['db'] += daraw
    gradients['dWax'] += np.dot(daraw, x.T)
    gradients['dWaa'] += np.dot(daraw, a_prev.T)
    gradients['da_next'] = np.dot(parameters['Waa'].T, daraw)
    return gradients

def rnn_backward(X, Y, parameters, cache):
    # Initialize gradients as an empty dictionary
    gradients = {}

    # Retrieve from cache and parameters
    (y_hat, a, x) = cache
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']

    # each one should be initialized to zeros of the same dimension as its corresponding parameter
    gradients['dWax'], gradients['dWaa'], gradients['dWya'] = np.zeros_like(Wax), np.zeros_like(Waa), np.zeros_like(Wya)
    gradients['db'], gradients['dby'] = np.zeros_like(b), np.zeros_like(by)
    gradients['da_next'] = np.zeros_like(a[0])

    ### START CODE HERE ###
    # Backpropagate through time
    for t in reversed(range(len(X))):
        dy = np.copy(y_hat[t])
        # this means, subract the correct answer from the predicted value (1-the predicted value which is specified by Y[t])
        dy[Y[t]] -= 1
        gradients = rnn_step_backward(dy, gradients, parameters, x[t], a[t], a[t-1])
    ### END CODE HERE ###

    return gradients, a

, и это моя реализация:

def rnn_cell_backward(self, xt, h, h_prev, output, true_label, dh_next):
    """
        Runs a single backward pass once.
        Inputs:
        - xt: The input data of shape (Batch_size, input_dim_size)
        - h:  The next hidden state at timestep t(which comes from the forward pass)
        - h_prev: The previous hidden state at timestep t-1
        - output : The output at the current timestep
        - true_label: The label for the current timestep, used for calculating loss
        - dh_next: The gradient of hidden state h (dh) which in the beginning
            is zero and is updated as we go backward in the backprogagation.
            the dh for the next round, would come from the 'dh_prev' as we will see shortly!
            Just remember the backward pass is essentially a loop! and we start at the end 
            and traverse back to the beginning!

        Returns : 
        - dW1 : The gradient for W1
        - dW2 : The gradient for W2
        - dW3 : The gradient for W3
        - dbh : The gradient for bh
        - dbo : The gradient for bo
        - dh_prev : The gradient for previous hiddenstate at timestep t-1. this will be used
        as the next dh for the next round of backpropagation.
        - per_ts_loss  : The loss for current timestep.
    """
    e = np.copy(output)
    # correct idx for each row(sample)!
    idxs = np.argmax(true_label, axis=1)
    # number of rows(samples) in our batch
    rows = np.arange(e.shape[0])
    # This is the vectorized version of error_t = output_t - label_t or simply e = output[t] - 1
    # where t refers to the index in which label is 1. 
    e[rows, idxs] -= 1
    # This is used for our loss to see how well we are doing during training.
    per_ts_loss = output[rows, idxs].sum()

    # must have shape of W3 which is (vocabsize_or_output_dim_size, hidden_state_size)
    dW3 = np.dot(e.T, h)
    # dbo = e.1, since we have batch we use np.sum
    # e is a vector, when it is subtracted from label, the result will be added to dbo
    dbo = np.sum(e, axis=0)
    # when calculating the dh, we also add the dh from the next timestep as well
    # when we are in the last timestep, the dh_next is initially zero.
    dh = np.dot(e,  self.W3) + dh_next  # from later cell
    # the input part
    dtanh = (1 - h * h) * dh
    # dbh = dtanh.1, we use sum, since we have a batch
    dbh = np.sum(dtanh, axis=0)

    # compute the gradient of the loss with respect to W1
    # this is actually not needed! we only care about tune-able
    # parameters, so we are only after, W1,W2,W3, db and do
    # dxt = np.dot(dtanh, W1.T)

    # must have the shape of (vocab_size, hidden_state_size)
    dW1 = np.dot(xt.T, dtanh)

    # compute the gradient with respect to W2
    dh_prev = np.dot(dtanh, self.W2)
    # shape must be (HiddenSize, HiddenSize)
    dW2 = np.dot(h_prev.T, dtanh)

    return dW1, dW2, dW3, dbh, dbo, dh_prev, per_ts_loss

def rnn_layer_backward(self, Xt, labels, H, O):
    """
        Runs a full backward pass on the given data. and returns the gradients.
        Inputs: 
        - Xt: The input data of shape (Batch_size, timesteps, input_dim_size)
        - labels: The labels for the input data
        - H: The hiddenstates for the current layer prodced in the foward pass 
          of shape (Batch_size, timesteps, HiddenStateSize)
        - O: The output for the current layer of shape (Batch_size, timesteps, outputsize)

        Returns :
        - dW1: The gradient for W1
        - dW2: The gradient for W2
        - dW3: The gradient for W3
        - dbh: The gradient for bh
        - dbo: The gradient for bo
        - dh: The gradient for the hidden state at timestep t
        - loss: The current loss 

    """

    dW1 = np.zeros_like(self.W1)
    dW2 = np.zeros_like(self.W2)
    dW3 = np.zeros_like(self.W3)
    dbh = np.zeros_like(self.bh)
    dbo = np.zeros_like(self.bo)
    dh_next = np.zeros_like(H[:, 0, :])
    hprev = None

    _, T_x, _ = Xt.shape
    loss = 0
    for t in reversed(range(T_x)):

        # this if-else block can be removed! and for hprev, we can simply
        # use H[:,t -1, : ] instead, but I also add this in case it makes a
        # a difference! so far I have not seen any difference though!
        if t > 0:
            hprev = H[:, t - 1, :]
        else:
            hprev = np.zeros_like(H[:, 0, :])

        dw_1, dw_2, dw_3, db_h, db_o, dh_prev, e = self.rnn_cell_backward(Xt[:, t, :],
                                                                          H[:, t, :],
                                                                          hprev,
                                                                          O[:, t, :],
                                                                          labels[:, t, :],
                                                                          dh_next)
        dh_next = dh_prev
        dW1 += dw_1
        dW2 += dw_2
        dW3 += dw_3
        dbh += db_h
        dbo += db_o

        # Update the loss by substracting the cross-entropy term of this time-step from it.
        loss -= np.log(e)

    return dW1, dW2, dW3, dbh, dbo, dh_next, loss

Я прокомментировал все и предоставилминимальный пример, чтобы продемонстрировать это здесь:
Мой код : (не проходит проверку градиента)

А вот реализация, которую я использовал в качестве руководства,Это из karpathy / Coursera и проходит все проверки градиента !: оригинальный код

На данный момент я совершенно не понимаю, так какЯ понятия не имею, почему это не работает!Я новичок в Python, поэтому, может быть, поэтому я не могу найти проблему!

1 Ответ

0 голосов
/ 19 июня 2019

2 месяца спустя я думаю, что нашел виновника! Я должен был изменить следующую строку:

# compute the gradient with respect to W2
dh_prev = np.dot(dtanh, self.W2)

до

# compute the gradient with respect to W2
# note the transpose here!
dh_prev = np.dot(dtanh, self.W2.T) 

Когда я первоначально писал обратный проход, я обращал внимание только на размеры, и это заставило меня совершить эту ошибку. Это на самом деле пример беспорядочных функций, которые могут произойти при бессмысленном / слепом изменении / транспонировании (или не делать этого!)
Чтобы понять, что пошло не так, позвольте мне привести пример.
Предположим, у нас есть матрица особенностей людей, и мы посвятили каждую строку каждому человеку, поэтому наша матрица будет выглядеть так:

      Features |  Age  | height(cm)  |  weight(kg)  | 
matrix =       |   20  |    185      |      75      |
               |   85  |    155      |      95      |
               |   40  |    205      |     120      |

Теперь, если мы превратим это в пустой массив, у нас будет следующее:

m = np.array([[20, 185, 75],
             [85, 155, 95],
             [40, 205, 120]])

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

 m1 = np.array([[20, 185, 75],
             [85, 155, 95],
             [40, 205, 120]])

 m2 = np.array([[0.9, 0.8, 0.85],
                [0.1, 0.5, 0.4],
                [0.6, 0.9, 0.8]])

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

...