tf.data: Combining multiple from_generator() datasets to create batches padded across time windows

Viewed 1175

I am working on a timeseries problem where each timeseries is fairly long (10^3-10^4 timesteps, and each timeseries is of different length).

For each sequence, I can define a Python generator that yields values one timestep at a time. I am using the tf.data.Dataset.from_generator() constructor to wrap these generators into the tf.data API. The documentation suggests using from_generator() along with the tf.contrib.data.parallel_interleave() transformation to parallelize the extraction from my Python generators.

My downstream use for these data is a stateful RNN (e.g. LSTM or GRU). I want to chunk up the timeseries into smaller (~10^2) windows and use each chunk as a training example (i.e., truncated BPTT). Since my data are streaming, I think that means saving up window_size timesteps of each generator before passing it on through the pipeline, to be batched with the other generators' data. I also want to save the RNN state across these chunks so I can still learn long-term dependencies.

My issue comes with wanting to create padded batches of these generators' batched outputs. Ideally, I would want to present to my neural network windows of the generator outputs, with padding as necessary when some subset of the generators exhaust themselves before others. I know that if I consume the entire generator output for each generator, then use Dataset.padded_batch() I can do this (and can then slice the padded batch across the time dimension into windowed chunks as necessary). However, I want to pass each window to the neural network it becomes available. If one of the generators exhausts itself before the others, I want to pad it with the padding value until all others have, so I can reset the RNN state and begin the next batch of generators with an empty initial RNN state. I am stuck here because the dataset resulting from tf.contrib.data.parallel_interleave() transformation discards each generator when it becomes exhausted, and the timeseries do not maintain a consistent ordering across samples from it.

Here is a small example:

import tensorflow as tf

def stepwise_generator(length):
    for i in range(length):
        yield i

lengths = list(range(1,10,2)) # [1, 3, 5, 7, 9]

window_length = 4
batch_size = 3

dataset = tf.data.Dataset.from_tensor_slices(lengths)

gen = lambda length: tf.data.Dataset.from_generator(
    stepwise_generator, tf.float32, output_shapes=[], args=(length,)
).batch(window_length) # this batching saves window_length timesteps per generator

dataset = dataset.apply(
    tf.contrib.data.parallel_interleave(gen, cycle_length=batch_size)
)

dataset = dataset.padded_batch(batch_size, (-1,), np.inf)
# batching 3 generators at once, and padding exhausted ones with inf.
# using a batch_size value no more than cycle_length above means we
# shouldn't start a new generator mid-batch (i think)

iterator = dataset.make_one_shot_iterator()
tensor = iterator.get_next()

outs = []
with tf.Session() as sess:
    while True:
        try:
            out = sess.run(tensor)
            outs.append(out)
        except tf.errors.OutOfRangeError:
            break

print(np.asarray(outs))

Output:

[[[ 0. inf inf inf]   # batch 1
  [ 0.  1.  2. inf]
  [ 0.  1.  2.  3.]]

 [[ 4. inf inf inf]   # batch 2 - the generator in index -1 in the
  [ 0.  1.  2.  3.]   # previous batch gets cycled to index 0 and two
  [ 0.  1.  2.  3.]]  # new generators are initiated

 [[ 4.  5.  6. inf]   # batch 3 - more generator cycling, and the one in
  [ 4.  5.  6.  7.]   # index 1 also gets cycled to index 2 in the same
  [ 8. inf inf inf]]] # batch (because we have run out of generators in
                      # parallel_interleave)

My desired output would be something like

[[[ 0. inf inf inf]   # batch 1
  [ 0.  1.  2. inf]
  [ 0.  1.  2.  3.]]

 [[inf]               # batch 2 - the leftover timestep from a padded 
  [inf]               # batch of the first 3 generators
  [4. ]]

 [[ 0.  1.  2.  3.]   # batch 3 - only two generators are left so this is 
  [ 0.  1.  2.  3.]]  # an end-of-epoch smaller batch

 [[ 4.  5.  6. inf]   # batch 4
  [ 4.  5.  6.  7.]]

 [[inf]               # batch 5
  [ 8.]]]

Here, the internal states of the RNNs would be reset after batch 2 and 5.

Again, the desired output can be simple to create if I consume the entirety of each generator's output, then pad, batch, and slice, but I want to produce batches as the generators, which may be each receiving data in real-time from e.g. a separate simulation, make them available.

1 Answers

Stateful RNNs in TensorFlow require the batch size to be fixed, so your desired output would not work: the batch size changes from 3 to 2.

So you need to have something like this instead:

[[[ 0. inf inf inf]   # batch 1
  [ 0.  1.  2. inf]
  [ 0.  1.  2.  3.]]

 [[inf]               # batch 2 - the leftover timestep from a padded 
  [inf]               # batch of the first 3 generators
  [4. ]]

 [[ 0.  1.  2.  3.]   # batch 3 - only two generators are left
  [ 0.  1.  2.  3.]   # but we still need the same batch size
  [ inf inf inf inf]] # so this row of `inf` is needed

 [[ 4.  5.  6. inf]   # batch 4
  [ 4.  5.  6.  7.]
  [ inf inf inf inf]]

 [[inf]               # batch 5
  [ 8.]
  [inf]]]

I don't believe it's possible to do this using your interleave + padded_batch approach.

However, one approach that does work is to pad all the sequences to the same length. Here's a working example using TensorFlow 2.4.1 (it should work with other TF 2 versions):

import tensorflow as tf
import numpy as np

lengths = list(range(1,12,2)) # [1, 3, 5, 7, 9, 11]
max_length = max(lengths)

def stepwise_generator(length):
    for i in range(max_length):
        if i < length:
            yield float(i)
        else:
            yield np.inf

window_length = 4
batch_size = 3

dataset = tf.data.Dataset.from_tensor_slices(lengths)

gen = lambda length: tf.data.Dataset.from_generator(
    stepwise_generator, tf.float32, args=(length,)
).batch(window_length) # this batching saves window_length timesteps per generator

dataset = dataset.interleave(gen, cycle_length=batch_size)
dataset = dataset.batch(batch_size)

for batch in dataset:
    print(batch)

This gives the following output:

tf.Tensor(
[[ 0. inf inf inf]
 [ 0.  1.  2. inf]
 [ 0.  1.  2.  3.]], shape=(3, 4), dtype=float32)
tf.Tensor(
[[inf inf inf inf]
 [inf inf inf inf]
 [ 4. inf inf inf]], shape=(3, 4), dtype=float32)
tf.Tensor(
[[inf inf inf]
 [inf inf inf]
 [inf inf inf]], shape=(3, 3), dtype=float32)
tf.Tensor(
[[0. 1. 2. 3.]
 [0. 1. 2. 3.]
 [0. 1. 2. 3.]], shape=(3, 4), dtype=float32)
tf.Tensor(
[[ 4.  5.  6. inf]
 [ 4.  5.  6.  7.]
 [ 4.  5.  6.  7.]], shape=(3, 4), dtype=float32)
tf.Tensor(
[[inf inf inf]
 [ 8. inf inf]
 [ 8.  9. 10.]], shape=(3, 3), dtype=float32)

Notes:

  • the generator now handles the padding, so we use batch() instead of padded_batch()
  • the number of sequences must be a multiple of the batch size
  • the tf.contrib package was removed in TF 2, but tf.contrib.data.parallel_interleave() was promoted to the core API: you can now use dataset.interleave()
  • since this is TF 2, there's no need for dataset.make_one_shot_iterator(), or iterator.get_next() or tf.Session(), etc. It's much simpler.
Related