web-dev-qa-db-fra.com

parallélisation tf.data.Dataset.from_generator

J'ai un pipeline d'entrée non trivial qui from_generator est parfait pour ...

dataset = tf.data.Dataset.from_generator(complex_img_label_generator,
                                        (tf.int32, tf.string))
dataset = dataset.batch(64)
iter = dataset.make_one_shot_iterator()
imgs, labels = iter.get_next()

complex_img_label_generator génère dynamiquement des images et renvoie un tableau numpy représentant un (H, W, 3) image et une simple étiquette string. Le traitement n'est pas quelque chose que je peux représenter comme la lecture de fichiers et tf.image opérations.

Ma question concerne la mise en parallèle du générateur? Comment puis-je avoir N de ces générateurs en cours d'exécution dans leurs propres threads.

Une idée était d’utiliser dataset.map avec num_parallel_calls pour gérer le filetage; mais la carte fonctionne sur des tenseurs ... Une autre idée était de créer plusieurs générateurs chacun avec son propre prefetch et de les joindre en quelque sorte, mais je ne vois pas comment je rejoindrais N flux de générateurs?

Des exemples canoniques que je pourrais suivre?

25
mat kelcey

Il s'avère que je peux utiliser Dataset.map si je rend le générateur super léger (ne générant que des métadonnées) et que je déplace ensuite l'éclairage lourd réel dans une fonction sans état. De cette façon, je peux paralléliser uniquement la partie de levage lourde avec .map utilisant un py_func.

Travaux; mais se sent un peu maladroit ... Ce serait génial de pouvoir simplement ajouter num_parallel_calls à from_generator :)

def pure_numpy_and_pil_complex_calculation(metadata, label):
  # some complex pil and numpy work nothing to do with tf
  ...

dataset = tf.data.Dataset.from_generator(lightweight_generator,
                                         output_types=(tf.string,   # metadata
                                                       tf.string))  # label

def wrapped_complex_calulation(metadata, label):
  return tf.py_func(func = pure_numpy_and_pil_complex_calculation,
                    inp = (metadata, label),
                    Tout = (tf.uint8,    # (H,W,3) img
                            tf.string))  # label
dataset = dataset.map(wrapped_complex_calulation,
                      num_parallel_calls=8)

dataset = dataset.batch(64)
iter = dataset.make_one_shot_iterator()
imgs, labels = iter.get_next()
21
mat kelcey

Je travaille sur un from_indexable Pour tf.data.Datasethttps://github.com/tensorflow/tensorflow/issues/14448

L'avantage de from_indexable Est qu'il peut être parallélisé, tandis qu'un générateur python ne peut pas être parallélisé.

La fonction from_indexable Crée un tf.data.range, Encapsule l'indexable dans un tf.py_func Généralisé et appelle la carte.

Pour ceux qui veulent maintenant un from_indexable, Voici le code lib

import tensorflow as tf
import numpy as np

from tensorflow.python.framework import tensor_shape
from tensorflow.python.util import nest

def py_func_decorator(output_types=None, output_shapes=None, stateful=True, name=None):
    def decorator(func):
        def call(*args):
            nonlocal output_shapes

            flat_output_types = nest.flatten(output_types)
            flat_values = tf.py_func(
                func, 
                inp=args, 
                Tout=flat_output_types,
                stateful=stateful, name=name
            )
            if output_shapes is not None:
                # I am not sure if this is nessesary
                output_shapes = nest.map_structure_up_to(
                    output_types, tensor_shape.as_shape, output_shapes)
                flattened_shapes = nest.flatten_up_to(output_types, output_shapes)
                for ret_t, shape in Zip(flat_values, flattened_shapes):
                    ret_t.set_shape(shape)
            return nest.pack_sequence_as(output_types, flat_values)
        return call
    return decorator

def from_indexable(iterator, output_types, output_shapes=None, num_parallel_calls=None, stateful=True, name=None):
    ds = tf.data.Dataset.range(len(iterator))
    @py_func_decorator(output_types, output_shapes, stateful=stateful, name=name)
    def index_to_entry(index):
        return iterator[index]    
    return ds.map(index_to_entry, num_parallel_calls=num_parallel_calls)

et voici un exemple (Remarque: from_indexable a un num_parallel_calls argument)

class PyDataSet:
    def __len__(self):
        return 20

    def __getitem__(self, item):
        return np.random.normal(size=(item+1, 10))

ds = from_indexable(PyDataSet(), output_types=tf.float64, output_shapes=[None, 10])
it = ds.make_one_shot_iterator()
entry = it.get_next()
with tf.Session() as sess:
    print(sess.run(entry).shape)
    print(sess.run(entry).shape)

Mise à jour 10 juin 2018: depuis https://github.com/tensorflow/tensorflow/pull/15121 est fusionné, le code de from_indexable se simplifie pour:

import tensorflow as tf

def py_func_decorator(output_types=None, output_shapes=None, stateful=True, name=None):
    def decorator(func):
        def call(*args, **kwargs):
            return tf.contrib.framework.py_func(
                func=func, 
                args=args, kwargs=kwargs, 
                output_types=output_types, output_shapes=output_shapes, 
                stateful=stateful, name=name
            )
        return call
    return decorator

def from_indexable(iterator, output_types, output_shapes=None, num_parallel_calls=None, stateful=True, name=None):
    ds = tf.data.Dataset.range(len(iterator))
    @py_func_decorator(output_types, output_shapes, stateful=stateful, name=name)
    def index_to_entry(index):
        return iterator[index]    
    return ds.map(index_to_entry, num_parallel_calls=num_parallel_calls)
7
Christoph Böddeker

Il est judicieux de limiter au minimum le travail effectué dans le generator et de paralléliser le traitement coûteux à l'aide d'un map.

Vous pouvez également "joindre" plusieurs générateurs à l'aide de parallel_interleave comme suit:

 def générateur (n): 
 # retourne la nième fonction de générateur 
 
 def jeu de données (n): 
 return tf.data.Dataset .from_generator (générateur (n)) 
 
 ds = tf.data.Dataset.range (N) .apply (tf.contrib.data.parallel_interleave (jeu de données, cycle_lenght = N)) 
 
 # où N est le nombre de générateurs que vous utilisez 
3
jsimsa