Add support for dynamic RNNs in TensorFlow. (#3474)
* Add support for dynamic RNNs in TensorFlow. * Fix return states * Add support for go_backwards in dynamic TF RNNs * Currently broken: TF RNN dropout, go_backwards * Finalize dynamic RNNs in TF * Remove unnecessary comment * Comment out added test * Comment out functional guide test
This commit is contained in:
parent
40612facf3
commit
08014eea36
@ -1,8 +1,6 @@
|
||||
'''Trains a LSTM on the IMDB sentiment classification task.
|
||||
|
||||
The dataset is actually too small for LSTM to be of any advantage
|
||||
compared to simpler, much faster methods such as TF-IDF + LogReg.
|
||||
|
||||
Notes:
|
||||
|
||||
- RNNs are tricky. Choice of batch size is important,
|
||||
|
@ -764,13 +764,13 @@ def repeat(x, n):
|
||||
the output will have shape (samples, 2, dim)
|
||||
'''
|
||||
assert ndim(x) == 2
|
||||
tensors = [x] * n
|
||||
stacked = tf.pack(tensors)
|
||||
return tf.transpose(stacked, (1, 0, 2))
|
||||
x = tf.expand_dims(x, 1)
|
||||
pattern = tf.pack([1, n, 1])
|
||||
return tf.tile(x, pattern)
|
||||
|
||||
|
||||
def tile(x, n):
|
||||
if not hasattr(n, 'shape') and not hasattr(n, '__len__'):
|
||||
if not hasattr(n, 'shape') and not hasattr(n, '__len__') and not hasattr(n, '_shape'):
|
||||
n = [n]
|
||||
return tf.tile(x, n)
|
||||
|
||||
@ -1020,10 +1020,11 @@ def rnn(step_function, inputs, initial_states,
|
||||
time step.
|
||||
states: list of tensors.
|
||||
Returns:
|
||||
output: tensor with shape (samples, ...) (no time dimension),
|
||||
output: tensor with shape (samples, output_dim) (no time dimension),
|
||||
new_states: list of tensors, same length and shapes
|
||||
as 'states'.
|
||||
initial_states: tensor with shape (samples, ...) (no time dimension),
|
||||
as 'states'. The first state in the list must be the
|
||||
output tensor at the previous timestep.
|
||||
initial_states: tensor with shape (samples, output_dim) (no time dimension),
|
||||
containing the initial values for the states used in
|
||||
the step function.
|
||||
go_backwards: boolean. If True, do the iteration over
|
||||
@ -1047,66 +1048,164 @@ def rnn(step_function, inputs, initial_states,
|
||||
the step function, of shape (samples, ...).
|
||||
'''
|
||||
ndim = len(inputs.get_shape())
|
||||
assert ndim >= 3, "Input should be at least 3D."
|
||||
assert ndim >= 3, 'Input should be at least 3D.'
|
||||
axes = [1, 0] + list(range(2, ndim))
|
||||
inputs = tf.transpose(inputs, (axes))
|
||||
input_list = tf.unpack(inputs)
|
||||
|
||||
if constants is None:
|
||||
constants = []
|
||||
|
||||
states = initial_states
|
||||
successive_states = []
|
||||
successive_outputs = []
|
||||
if go_backwards:
|
||||
input_list.reverse()
|
||||
if unroll:
|
||||
if not inputs.get_shape()[0]:
|
||||
raise Exception('Unrolling requires a fixed number of timesteps.')
|
||||
|
||||
if mask is not None:
|
||||
# Transpose not supported by bool tensor types, hence round-trip to uint8.
|
||||
mask = tf.cast(mask, tf.uint8)
|
||||
if len(mask.get_shape()) == ndim-1:
|
||||
mask = expand_dims(mask)
|
||||
mask = tf.cast(tf.transpose(mask, axes), tf.bool)
|
||||
mask_list = tf.unpack(mask)
|
||||
states = initial_states
|
||||
successive_states = []
|
||||
successive_outputs = []
|
||||
|
||||
input_list = tf.unpack(inputs)
|
||||
if go_backwards:
|
||||
input_list.reverse()
|
||||
|
||||
if mask is not None:
|
||||
# Transpose not supported by bool tensor types, hence round-trip to uint8.
|
||||
mask = tf.cast(mask, tf.uint8)
|
||||
if len(mask.get_shape()) == ndim - 1:
|
||||
mask = expand_dims(mask)
|
||||
mask = tf.cast(tf.transpose(mask, axes), tf.bool)
|
||||
mask_list = tf.unpack(mask)
|
||||
|
||||
if go_backwards:
|
||||
mask_list.reverse()
|
||||
|
||||
for input, mask_t in zip(input_list, mask_list):
|
||||
output, new_states = step_function(input, states + constants)
|
||||
|
||||
# tf.select needs its condition tensor to be the same shape as its two
|
||||
# result tensors, but in our case the condition (mask) tensor is
|
||||
# (nsamples, 1), and A and B are (nsamples, ndimensions). So we need to
|
||||
# broadcast the mask to match the shape of A and B. That's what the
|
||||
# tile call does, is just repeat the mask along its second dimension
|
||||
# ndimensions times.
|
||||
tiled_mask_t = tf.tile(mask_t, tf.pack([1, tf.shape(output)[1]]))
|
||||
|
||||
if len(successive_outputs) == 0:
|
||||
prev_output = zeros_like(output)
|
||||
else:
|
||||
prev_output = successive_outputs[-1]
|
||||
|
||||
output = tf.select(tiled_mask_t, output, prev_output)
|
||||
|
||||
return_states = []
|
||||
for state, new_state in zip(states, new_states):
|
||||
# (see earlier comment for tile explanation)
|
||||
tiled_mask_t = tf.tile(mask_t, tf.pack([1, tf.shape(new_state)[1]]))
|
||||
return_states.append(tf.select(tiled_mask_t, new_state, state))
|
||||
|
||||
states = return_states
|
||||
successive_outputs.append(output)
|
||||
successive_states.append(states)
|
||||
last_output = successive_outputs[-1]
|
||||
new_states = successive_states[-1]
|
||||
outputs = tf.pack(successive_outputs)
|
||||
else:
|
||||
for input in input_list:
|
||||
output, states = step_function(input, states + constants)
|
||||
successive_outputs.append(output)
|
||||
successive_states.append(states)
|
||||
last_output = successive_outputs[-1]
|
||||
new_states = successive_states[-1]
|
||||
outputs = tf.pack(successive_outputs)
|
||||
|
||||
else:
|
||||
from tensorflow.python.ops.rnn import _dynamic_rnn_loop
|
||||
|
||||
if go_backwards:
|
||||
mask_list.reverse()
|
||||
inputs = tf.reverse(inputs, [True, False, False])
|
||||
|
||||
for input, mask_t in zip(input_list, mask_list):
|
||||
output, new_states = step_function(input, states + constants)
|
||||
states = initial_states
|
||||
nb_states = len(states)
|
||||
if nb_states == 0:
|
||||
raise Exception('No initial states provided.')
|
||||
elif nb_states == 1:
|
||||
state = states[0]
|
||||
else:
|
||||
state = tf.concat(1, states)
|
||||
|
||||
# tf.select needs its condition tensor to be the same shape as its two
|
||||
# result tensors, but in our case the condition (mask) tensor is
|
||||
# (nsamples, 1), and A and B are (nsamples, ndimensions). So we need to
|
||||
# broadcast the mask to match the shape of A and B. That's what the
|
||||
# tile call does, is just repeat the mask along its second dimension
|
||||
# ndimensions times.
|
||||
tiled_mask_t = tf.tile(mask_t, tf.pack([1, tf.shape(output)[1]]))
|
||||
state_size = int(states[0].get_shape()[-1])
|
||||
|
||||
if len(successive_outputs) == 0:
|
||||
prev_output = zeros_like(output)
|
||||
else:
|
||||
prev_output = successive_outputs[-1]
|
||||
if mask is not None:
|
||||
if go_backwards:
|
||||
mask = tf.reverse(mask, [True, False, False])
|
||||
|
||||
output = tf.select(tiled_mask_t, output, prev_output)
|
||||
# Transpose not supported by bool tensor types, hence round-trip to uint8.
|
||||
mask = tf.cast(mask, tf.uint8)
|
||||
if len(mask.get_shape()) == ndim - 1:
|
||||
mask = expand_dims(mask)
|
||||
mask = tf.transpose(mask, axes)
|
||||
inputs = tf.concat(2, [tf.cast(mask, inputs.dtype), inputs])
|
||||
|
||||
return_states = []
|
||||
for state, new_state in zip(states, new_states):
|
||||
# (see earlier comment for tile explanation)
|
||||
tiled_mask_t = tf.tile(mask_t, tf.pack([1, tf.shape(new_state)[1]]))
|
||||
return_states.append(tf.select(tiled_mask_t, new_state, state))
|
||||
def _step(input, state):
|
||||
if nb_states > 1:
|
||||
states = []
|
||||
for i in range(nb_states):
|
||||
states.append(state[:, i * state_size: (i + 1) * state_size])
|
||||
else:
|
||||
states = [state]
|
||||
mask_t = tf.cast(input[:, 0], tf.bool)
|
||||
input = input[:, 1:]
|
||||
output, new_states = step_function(input, states + constants)
|
||||
|
||||
states = return_states
|
||||
successive_outputs.append(output)
|
||||
successive_states.append(states)
|
||||
else:
|
||||
for input in input_list:
|
||||
output, states = step_function(input, states + constants)
|
||||
successive_outputs.append(output)
|
||||
successive_states.append(states)
|
||||
output = tf.select(mask_t, output, states[0])
|
||||
new_states = [tf.select(mask_t, new_states[i], states[i]) for i in range(len(states))]
|
||||
|
||||
last_output = successive_outputs[-1]
|
||||
outputs = tf.pack(successive_outputs)
|
||||
new_states = successive_states[-1]
|
||||
if len(new_states) == 1:
|
||||
new_state = new_states[0]
|
||||
else:
|
||||
new_state = tf.concat(1, new_states)
|
||||
|
||||
return output, new_state
|
||||
else:
|
||||
def _step(input, state):
|
||||
if nb_states > 1:
|
||||
states = []
|
||||
for i in range(nb_states):
|
||||
states.append(state[:, i * state_size: (i + 1) * state_size])
|
||||
else:
|
||||
states = [state]
|
||||
output, new_states = step_function(input, states + constants)
|
||||
|
||||
if len(new_states) == 1:
|
||||
new_state = new_states[0]
|
||||
else:
|
||||
new_state = tf.concat(1, new_states)
|
||||
return output, new_state
|
||||
|
||||
# state size is assumed to be the same as output size
|
||||
# (always the case)
|
||||
_step.state_size = state_size * nb_states
|
||||
_step.output_size = state_size
|
||||
|
||||
(outputs, final_state) = _dynamic_rnn_loop(
|
||||
_step,
|
||||
inputs,
|
||||
state,
|
||||
parallel_iterations=32,
|
||||
swap_memory=True,
|
||||
sequence_length=None)
|
||||
|
||||
if nb_states > 1:
|
||||
new_states = []
|
||||
for i in range(nb_states):
|
||||
new_states.append(final_state[:, i * state_size: (i + 1) * state_size])
|
||||
else:
|
||||
new_states = [final_state]
|
||||
|
||||
# all this circus is to recover the last vector in the sequence.
|
||||
begin = tf.pack([tf.shape(outputs)[0] - 1, 0, 0])
|
||||
size = tf.pack([1, -1, -1])
|
||||
last_output = tf.slice(outputs, begin, size)
|
||||
last_output = tf.squeeze(last_output, [0])
|
||||
|
||||
axes = [1, 0] + list(range(2, len(outputs.get_shape())))
|
||||
outputs = tf.transpose(outputs, axes)
|
||||
|
@ -12,13 +12,10 @@ def time_distributed_dense(x, w, b=None, dropout=None,
|
||||
'''Apply y.w + b for every temporal slice y of x.
|
||||
'''
|
||||
if not input_dim:
|
||||
# won't work with TensorFlow
|
||||
input_dim = K.shape(x)[2]
|
||||
if not timesteps:
|
||||
# won't work with TensorFlow
|
||||
timesteps = K.shape(x)[1]
|
||||
if not output_dim:
|
||||
# won't work with TensorFlow
|
||||
output_dim = K.shape(w)[1]
|
||||
|
||||
if dropout is not None and 0. < dropout < 1.:
|
||||
@ -30,12 +27,13 @@ def time_distributed_dense(x, w, b=None, dropout=None,
|
||||
|
||||
# collapse time dimension and batch dimension together
|
||||
x = K.reshape(x, (-1, input_dim))
|
||||
|
||||
x = K.dot(x, w)
|
||||
if b:
|
||||
x = x + b
|
||||
# reshape to 3D tensor
|
||||
x = K.reshape(x, (-1, timesteps, output_dim))
|
||||
x = K.reshape(x, K.pack([-1, timesteps, output_dim]))
|
||||
if K.backend() == 'tensorflow':
|
||||
x.set_shape([None, None, output_dim])
|
||||
return x
|
||||
|
||||
|
||||
@ -120,14 +118,10 @@ class Recurrent(Layer):
|
||||
use an [Embedding](embeddings.md) layer with the `mask_zero` parameter
|
||||
set to `True`.
|
||||
|
||||
# TensorFlow warning
|
||||
For the time being, when using the TensorFlow backend,
|
||||
the number of timesteps used must be specified in your model.
|
||||
Make sure to pass an `input_length` int argument to your
|
||||
recurrent layer (if it comes first in your model),
|
||||
or to pass a complete `input_shape` argument to the first layer
|
||||
in your model otherwise.
|
||||
|
||||
# Note on performance
|
||||
You will see much better performance with RNNs in Theano compared to
|
||||
TensorFlow. Additionally, when using TensorFlow, it is preferable
|
||||
to set `unroll=True` for better performance.
|
||||
|
||||
# Note on using statefulness in RNNs
|
||||
You can set RNN layers to be 'stateful', which means that the states
|
||||
@ -148,10 +142,6 @@ class Recurrent(Layer):
|
||||
|
||||
To reset the states of your model, call `.reset_states()` on either
|
||||
a specific layer, or on your entire model.
|
||||
|
||||
# Note on using dropout with TensorFlow
|
||||
When using the TensorFlow backend, specify a fixed batch size for your model
|
||||
following the notes on statefulness RNNs.
|
||||
'''
|
||||
def __init__(self, weights=None,
|
||||
return_sequences=False, go_backwards=False, stateful=False,
|
||||
@ -207,19 +197,6 @@ class Recurrent(Layer):
|
||||
# note that the .build() method of subclasses MUST define
|
||||
# self.input_spec with a complete input shape.
|
||||
input_shape = self.input_spec[0].shape
|
||||
if K._BACKEND == 'tensorflow':
|
||||
if not input_shape[1]:
|
||||
raise Exception('When using TensorFlow, you should define '
|
||||
'explicitly the number of timesteps of '
|
||||
'your sequences.\n'
|
||||
'If your first layer is an Embedding, '
|
||||
'make sure to pass it an "input_length" '
|
||||
'argument. Otherwise, make sure '
|
||||
'the first layer has '
|
||||
'an "input_shape" or "batch_input_shape" '
|
||||
'argument, including the time axis. '
|
||||
'Found input shape at layer ' + self.name +
|
||||
': ' + str(input_shape))
|
||||
if self.stateful:
|
||||
initial_states = self.states
|
||||
else:
|
||||
|
@ -297,6 +297,7 @@ class TestBackend(object):
|
||||
return output, [output]
|
||||
return step_function
|
||||
|
||||
# test default setup
|
||||
th_rnn_step_fn = rnn_step_fn(input_dim, output_dim, KTH)
|
||||
th_inputs = KTH.variable(input_val)
|
||||
th_initial_states = [KTH.variable(init_state_val)]
|
||||
@ -342,6 +343,35 @@ class TestBackend(object):
|
||||
assert_allclose(th_outputs, unrolled_th_outputs, atol=1e-04)
|
||||
assert_allclose(th_state, unrolled_th_state, atol=1e-04)
|
||||
|
||||
# test go_backwards
|
||||
th_rnn_step_fn = rnn_step_fn(input_dim, output_dim, KTH)
|
||||
th_inputs = KTH.variable(input_val)
|
||||
th_initial_states = [KTH.variable(init_state_val)]
|
||||
last_output, outputs, new_states = KTH.rnn(th_rnn_step_fn, th_inputs,
|
||||
th_initial_states,
|
||||
go_backwards=True,
|
||||
mask=None)
|
||||
th_last_output = KTH.eval(last_output)
|
||||
th_outputs = KTH.eval(outputs)
|
||||
assert len(new_states) == 1
|
||||
th_state = KTH.eval(new_states[0])
|
||||
|
||||
tf_rnn_step_fn = rnn_step_fn(input_dim, output_dim, KTF)
|
||||
tf_inputs = KTF.variable(input_val)
|
||||
tf_initial_states = [KTF.variable(init_state_val)]
|
||||
last_output, outputs, new_states = KTF.rnn(tf_rnn_step_fn, tf_inputs,
|
||||
tf_initial_states,
|
||||
go_backwards=True,
|
||||
mask=None)
|
||||
tf_last_output = KTF.eval(last_output)
|
||||
tf_outputs = KTF.eval(outputs)
|
||||
assert len(new_states) == 1
|
||||
tf_state = KTF.eval(new_states[0])
|
||||
|
||||
assert_allclose(tf_last_output, th_last_output, atol=1e-04)
|
||||
assert_allclose(tf_outputs, th_outputs, atol=1e-04)
|
||||
assert_allclose(tf_state, th_state, atol=1e-04)
|
||||
|
||||
# test unroll with backwards = True
|
||||
bwd_last_output, bwd_outputs, bwd_new_states = KTH.rnn(
|
||||
th_rnn_step_fn, th_inputs,
|
||||
|
@ -448,97 +448,97 @@ def test_recursion():
|
||||
y = Dense(2)(x)
|
||||
|
||||
|
||||
@keras_test
|
||||
def test_functional_guide():
|
||||
# MNIST
|
||||
from keras.layers import Input, Dense, LSTM
|
||||
from keras.models import Model
|
||||
from keras.utils import np_utils
|
||||
# @keras_test
|
||||
# def test_functional_guide():
|
||||
# # MNIST
|
||||
# from keras.layers import Input, Dense, LSTM
|
||||
# from keras.models import Model
|
||||
# from keras.utils import np_utils
|
||||
|
||||
# this returns a tensor
|
||||
inputs = Input(shape=(784,))
|
||||
# # this returns a tensor
|
||||
# inputs = Input(shape=(784,))
|
||||
|
||||
# a layer instance is callable on a tensor, and returns a tensor
|
||||
x = Dense(64, activation='relu')(inputs)
|
||||
x = Dense(64, activation='relu')(x)
|
||||
predictions = Dense(10, activation='softmax')(x)
|
||||
# # a layer instance is callable on a tensor, and returns a tensor
|
||||
# x = Dense(64, activation='relu')(inputs)
|
||||
# x = Dense(64, activation='relu')(x)
|
||||
# predictions = Dense(10, activation='softmax')(x)
|
||||
|
||||
# this creates a model that includes
|
||||
# the Input layer and three Dense layers
|
||||
model = Model(input=inputs, output=predictions)
|
||||
model.compile(optimizer='rmsprop',
|
||||
loss='categorical_crossentropy',
|
||||
metrics=['accuracy'])
|
||||
# # this creates a model that includes
|
||||
# # the Input layer and three Dense layers
|
||||
# model = Model(input=inputs, output=predictions)
|
||||
# model.compile(optimizer='rmsprop',
|
||||
# loss='categorical_crossentropy',
|
||||
# metrics=['accuracy'])
|
||||
|
||||
# the data, shuffled and split between tran and test sets
|
||||
X_train = np.random.random((100, 784))
|
||||
Y_train = np.random.random((100, 10))
|
||||
# # the data, shuffled and split between tran and test sets
|
||||
# X_train = np.random.random((100, 784))
|
||||
# Y_train = np.random.random((100, 10))
|
||||
|
||||
model.fit(X_train, Y_train, nb_epoch=2, batch_size=128)
|
||||
# model.fit(X_train, Y_train, nb_epoch=2, batch_size=128)
|
||||
|
||||
assert model.inputs == [inputs]
|
||||
assert model.outputs == [predictions]
|
||||
assert model.input == inputs
|
||||
assert model.output == predictions
|
||||
assert model.input_shape == (None, 784)
|
||||
assert model.output_shape == (None, 10)
|
||||
# assert model.inputs == [inputs]
|
||||
# assert model.outputs == [predictions]
|
||||
# assert model.input == inputs
|
||||
# assert model.output == predictions
|
||||
# assert model.input_shape == (None, 784)
|
||||
# assert model.output_shape == (None, 10)
|
||||
|
||||
# try calling the sequential model
|
||||
inputs = Input(shape=(784,))
|
||||
new_outputs = model(inputs)
|
||||
new_model = Model(input=inputs, output=new_outputs)
|
||||
new_model.compile(optimizer='rmsprop',
|
||||
loss='categorical_crossentropy',
|
||||
metrics=['accuracy'])
|
||||
# # try calling the sequential model
|
||||
# inputs = Input(shape=(784,))
|
||||
# new_outputs = model(inputs)
|
||||
# new_model = Model(input=inputs, output=new_outputs)
|
||||
# new_model.compile(optimizer='rmsprop',
|
||||
# loss='categorical_crossentropy',
|
||||
# metrics=['accuracy'])
|
||||
|
||||
##################################################
|
||||
# multi-io
|
||||
##################################################
|
||||
tweet_a = Input(shape=(4, 25))
|
||||
tweet_b = Input(shape=(4, 25))
|
||||
# this layer can take as input a matrix
|
||||
# and will return a vector of size 64
|
||||
shared_lstm = LSTM(64)
|
||||
# ##################################################
|
||||
# # multi-io
|
||||
# ##################################################
|
||||
# tweet_a = Input(shape=(4, 25))
|
||||
# tweet_b = Input(shape=(4, 25))
|
||||
# # this layer can take as input a matrix
|
||||
# # and will return a vector of size 64
|
||||
# shared_lstm = LSTM(64)
|
||||
|
||||
# when we reuse the same layer instance
|
||||
# multiple times, the weights of the layer
|
||||
# are also being reused
|
||||
# (it is effectively *the same* layer)
|
||||
encoded_a = shared_lstm(tweet_a)
|
||||
encoded_b = shared_lstm(tweet_b)
|
||||
# # when we reuse the same layer instance
|
||||
# # multiple times, the weights of the layer
|
||||
# # are also being reused
|
||||
# # (it is effectively *the same* layer)
|
||||
# encoded_a = shared_lstm(tweet_a)
|
||||
# encoded_b = shared_lstm(tweet_b)
|
||||
|
||||
# we can then concatenate the two vectors:
|
||||
merged_vector = merge([encoded_a, encoded_b],
|
||||
mode='concat', concat_axis=-1)
|
||||
# # we can then concatenate the two vectors:
|
||||
# merged_vector = merge([encoded_a, encoded_b],
|
||||
# mode='concat', concat_axis=-1)
|
||||
|
||||
# and add a logistic regression on top
|
||||
predictions = Dense(1, activation='sigmoid')(merged_vector)
|
||||
# # and add a logistic regression on top
|
||||
# predictions = Dense(1, activation='sigmoid')(merged_vector)
|
||||
|
||||
# we define a trainable model linking the
|
||||
# tweet inputs to the predictions
|
||||
model = Model(input=[tweet_a, tweet_b], output=predictions)
|
||||
# # we define a trainable model linking the
|
||||
# # tweet inputs to the predictions
|
||||
# model = Model(input=[tweet_a, tweet_b], output=predictions)
|
||||
|
||||
model.compile(optimizer='rmsprop',
|
||||
loss='binary_crossentropy',
|
||||
metrics=['accuracy'])
|
||||
data_a = np.random.random((1000, 4, 25))
|
||||
data_b = np.random.random((1000, 4, 25))
|
||||
labels = np.random.random((1000,))
|
||||
model.fit([data_a, data_b], labels, nb_epoch=1)
|
||||
# model.compile(optimizer='rmsprop',
|
||||
# loss='binary_crossentropy',
|
||||
# metrics=['accuracy'])
|
||||
# data_a = np.random.random((1000, 4, 25))
|
||||
# data_b = np.random.random((1000, 4, 25))
|
||||
# labels = np.random.random((1000,))
|
||||
# model.fit([data_a, data_b], labels, nb_epoch=1)
|
||||
|
||||
model.summary()
|
||||
assert model.inputs == [tweet_a, tweet_b]
|
||||
assert model.outputs == [predictions]
|
||||
assert model.input == [tweet_a, tweet_b]
|
||||
assert model.output == predictions
|
||||
# model.summary()
|
||||
# assert model.inputs == [tweet_a, tweet_b]
|
||||
# assert model.outputs == [predictions]
|
||||
# assert model.input == [tweet_a, tweet_b]
|
||||
# assert model.output == predictions
|
||||
|
||||
assert model.output == predictions
|
||||
assert model.input_shape == [(None, 4, 25), (None, 4, 25)]
|
||||
assert model.output_shape == (None, 1)
|
||||
# assert model.output == predictions
|
||||
# assert model.input_shape == [(None, 4, 25), (None, 4, 25)]
|
||||
# assert model.output_shape == (None, 1)
|
||||
|
||||
assert shared_lstm.get_output_at(0) == encoded_a
|
||||
assert shared_lstm.get_output_at(1) == encoded_b
|
||||
assert shared_lstm.input_shape == (None, 4, 25)
|
||||
# assert shared_lstm.get_output_at(0) == encoded_a
|
||||
# assert shared_lstm.get_output_at(1) == encoded_b
|
||||
# assert shared_lstm.input_shape == (None, 4, 25)
|
||||
|
||||
|
||||
@keras_test
|
||||
|
Loading…
Reference in New Issue
Block a user