I have a dataset consisting of an array of time series episodes. I have previously trained my neural network using a straightforward tf.keras.utils.Sequence, which windows each episode into equal length inputs with a definable shift. For example, data might look like this:
[
[1, 2, 3, 4, 5, 6, 7],
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
]
And the inputs after applying a sliding window would be like this:
[1, 2, 3, 4, 5 ],
[2, 3, 4, 5, 6 ],
[3, 4, 5, 6, 7 ],
[11, 12, 13, 14, 15],
[12, 13, 14, 15, 16],
...
[16, 17, 18, 19, 20]
So we get a series of fixed length input vectors, ready to be batched.
Each episode is stored in a file and loaded into memory using np.load. The Keras Sequence code is straight-forward, like this:
def get_segment(self, segment_locator):
filename = segment_locator[0]
wav_data = self.cache[filename]
r = wav_data[segment_locator[1]:segment_locator[2]]
assert len(r) == self.fragment_length, "For %s, expected segment length %d, got %d" % (segment_locator, self.fragment_length, len(r))
return r
def __getitem__(self, index):
batch_set = self.segment_list[index * self.batch_size:(index + 1) * self.batch_size]
batch_x = np.array([self.get_segment(segment_locator) for segment_locator in batch_set])
batch_y = batch_x
return batch_x, batch_y
Basically we have a "segment list" which is a precalculated list of segments identified by (filename, start, end) tuples. (End is in this case redundant since it will always be start + segment_length). We shuffle the list of segment selections, batch it and send that to model.fit.
I'm looking to switch to a multi-GPU setup and the recommendation I've read online is to use tf.data. I made a simple wrapper to convert my sequence to a tf dataset. Note that input == target in this example, so we zip together two copies of the same data sequence in the final step.
def straight_chunk_generator(self):
for filename in self.filenames:
for start_at in range(0, self.cache[filename].shape[0], self.shift):
end_at = start_at + self.fragment_length
if end_at > self.cache[filename].shape[0]:
break
yield self.cache[filename][start_at:end_at]
def as_tf_data(self):
x_ds = tf.data.Dataset.from_generator(self.straight_chunk_generator, output_types=tf.float32)
return tf.data.Dataset.zip((x_ds, x_ds))
The problem is that when I use this Dataset, epochs run about 5X as slowly. What's happening here? prefetch and cache seem to have no effect.
I also tried a more "pure" tf.data variation:
def as_tf_data0(self):
dataset = tf.data.Dataset.from_tensor_slices(tf.ragged.constant([self.cache[filename] for filename in self.filenames]))
def window_subseries(subarray):
return tf.data.Dataset.from_tensor_slices(subarray).window(self.fragment_length, shift=self.shift, drop_remainder=True)
x_ds = dataset.flat_map(window_subseries).flat_map(lambda x: x.batch(self.fragment_length))
return tf.data.Dataset.zip((x_ds, x_ds))
(This got oddly complicated because window doesn't return the original data shape, but a series of datasets so they have to be "unwrapped", which x.batch does. In theory.)
This version is also inexplicably slow.
I am having difficulty understanding why my code is running slowly. I understand that the tf.data.Dataset.from_generator function can be slow due to the Python GIL, but I don't understand why it would be so much slower than the pure Python Sequence. And shouldn't adding a .cache() to the pipeline effectively force ahead of time evaluation of that part anyhow?
If the generator is the problem, why didn't my "native" implementation using from_tensor_slices, flat_map etc improve things? I thought that would lead to some kind of C++ optimised execution.