Я некоторое время бился об этом головой и не могу понять, что я сделал неправильно (если что-то) при реализации этих RNN. Чтобы избавить вас, ребята, от прямой фазы, я могу сказать вам, что две реализации вычисляют одни и те же выходные данные, поэтому прямая фаза верна. Проблема в обратной фазе.
Вот мой обратный код Python. Он довольно точно следует стилю нейронного разговора Карпати, но не совсем:
def backward(self, cache, target,c=leastsquares_cost, dc=leastsquares_dcost):
'''
cache is from forward pass
c is a cost function
dc is a function used as dc(output, target) which gives the gradient dc/doutput
'''
XdotW = cache['XdotW'] #num_time_steps x hidden_size
Hin = cache['Hin'] # num_time_steps x hidden_size
T = Hin.shape[0]
Hout = cache['Hout']
Xin = cache['Xin']
Xout = cache['Xout']
Oin = cache['Oin'] # num_time_steps x output_size
Oout=cache['Oout']
dcdOin = dc(Oout, target) # this will be num_time_steps x num_outputs. these are dc/dO_j
dcdWho = np.dot(Hout.transpose(), dcdOin) # this is the sum of outer products for all time
# bias term is added at the end with coefficient 1 hence the dot product is just the sum
dcdbho = np.sum(dcdOin, axis=0, keepdims=True) #this sums all the time steps
dcdHout = np.dot(dcdOin, self.Who.transpose()) #reflects dcdHout_ij should be the dot product of dcdoin and the i'th row of Who; this is only for the outputs
# now go back in time
dcdHin = np.zeros(dcdHout.shape)
# for t=T we can ignore the other term (error from the next timestep). self.df is derivative of activation function (here, tanh):
dcdHin[T-1] = self.df(Hin[T-1]) * dcdHout[T-1] # because we don't need to worry about the next timestep, dcdHout is already corrent for t=T
for t in reversed(xrange(T-1)):
# we need to add to dcdHout[t] the error from the next timestep
dcdHout[t] += np.dot(dcdHin[t], self.Whh.transpose())
# now we have the correct form for dcdHout[t]
dcdHin[t] = self.df(Hin[t]) * dcdHout[t]
# now we've gone through all t, and we can continue
dcdWhh = np.zeros(self.Whh.shape)
for t in range(T-1): #skip T bc dHdin[T+1] doesn't exist
dcdWhh += np.outer(Hout[t], dcdHin[t+1])
# and we can do bias as well
dcdbhh = np.sum(dcdHin,axis=0, keepdims=True)
# now we need to go back to the embeddings
dcdWxh = np.dot(Xout.transpose(), dcdHin)
return {'dcdOout': dcdOout, 'dcdWxh': dcdWxh, 'dcdWhh': dcdWhh, 'dcdWho': dcdWho, 'dcdbhh': dcdbhh, 'dcdbho': dcdbho, 'cost':c(Oout, target)}
И вот код theano (в основном скопированный из другой реализации, которую я нашел в Интернете. Я инициализирую веса рандомизированными весами моего чистого Python rnn, чтобы все было одинаково):
# input (where first dimension is time)
u = TT.matrix()
# target (where first dimension is time)
t = TT.matrix()
# initial hidden state of the RNN
h0 = TT.vector()
# learning rate
lr = TT.scalar()
# recurrent weights as a shared variable
W = theano.shared(rnn.Whh)
# input to hidden layer weights
W_in = theano.shared(rnn.Wxh)
# hidden to output layer weights
W_out = theano.shared(rnn.Who)
# bias 1
b_h = theano.shared(rnn.bhh[0])
# bias 2
b_o = theano.shared(rnn.bho[0])
# recurrent function (using tanh activation function) and linear output
# activation function
def step(u_t, h_tm1, W, W_in, W_out):
h_t = TT.tanh(TT.dot(u_t, W_in) + TT.dot(h_tm1, W) + b_h)
y_t = TT.dot(h_t, W_out) + b_o
return h_t, y_t
# the hidden state `h` for the entire sequence, and the output for the
# entrie sequence `y` (first dimension is always time)
[h, y], _ = theano.scan(step,
sequences=u,
outputs_info=[h0, None],
non_sequences=[W, W_in, W_out])
# error between output and target
error = (.5*(y - t) ** 2).sum()
# gradients on the weights using BPTT
gW, gW_in, gW_out, gb_h, gb_o = TT.grad(error, [W, W_in, W_out, b_h, b_o])
# training function, that computes the error and updates the weights using
# SGD.
Теперь вот сумасшедшая вещь. Если я запускаю следующее:
fn = theano.function([h0, u, t, lr],
[error, y, h, gW, gW_in, gW_out, gb_h, gb_o],
updates={W: W - lr * gW,
W_in: W_in - lr * gW_in,
W_out: W_out - lr * gW_out})
er, yout, hout, gWhh, gWhx, gWho, gbh, gbo =fn(numpy.zeros((n,)), numpy.eye(5), numpy.eye(5),.01)
cache = rnn.forward(np.eye(5))
bc = rnn.backward(cache, np.eye(5))
print "sum difference between gWho (theano) and bc['dcdWho'] (pure python):"
print np.sum(gWho - bc['dcdWho'])
print "sum differnce between gWhh(theano) and bc['dcdWho'] (pure python):"
print np.sum(gWhh - bc['dcdWhh'])
print "sum difference between gWhx (theano) and bc['dcdWxh'] (pure pyython):"
print np.sum(gWhx - bc['dcdWxh'])
print "sum different between the last row of gWhx (theano) and the last row of bc['dcdWxh'] (pure python):"
print np.sum(gWhx[-1] - bc['dcdWxh'][-1])
Я получаю следующий вывод:
sum difference between gWho (theano) and bc['dcdWho'] (pure python):
-4.59268040265e-16
sum differnce between gWhh(theano) and bc['dcdWhh'] (pure python):
0.120527063611
sum difference between gWhx (theano) and bc['dcdWxh'] (pure pyython):
-0.332613468652
sum different between the last row of gWhx (theano) and the last row of bc['dcdWxh'] (pure python):
4.33680868994e-18
Итак, я получаю производные матрицы весов между скрытым слоем и выводом справа, но не производные матрицы весов скрыты -> скрыты или введены -> скрыты. Но эта безумная вещь заключается в том, что я ВСЕГДА получаю ПОСЛЕДНЮЮ СТРОКУ ввода матрицы веса -> скрыто правильно. Это безумие для меня. Я понятия не имею, что здесь происходит. Обратите внимание, что последняя строка входной матрицы весов -> скрытая НЕ соответствует последнему временному шагу или чему-то еще (это можно объяснить, например, тем, что я правильно вычислил производные для последнего временного шага, но не правильно распространяется обратно во времени). dcdWxh - это сумма dcdWxh по всем временным шагам -- так как же я могу получить правильную одну строку, но не другую???
Кто-нибудь может помочь? У меня закончились идеи.