web-dev-qa-db-fra.com

Diffusion en continu de fichiers volumineux de formation et de test dans DNNClassifier de Tensorflow

J'ai un énorme fichier CSV de formation (709M) et un grand fichier CSV de test (125M) que je souhaite envoyer dans un DNNClassifier dans le contexte de l'utilisation de l'API Tensorflow de haut niveau.

Il semble que les paramètres input_fn acceptés par fit et evaluate doivent contenir toutes les données de caractéristiques et d'étiquettes en mémoire, mais je voudrais actuellement l'exécuter sur ma machine locale et m'attendre donc à ce qu'il manque de mémoire assez rapidement si je lis ces fichiers. en mémoire et ensuite les traiter.

J'ai parcouru le document sur streamed-reading of data , mais l'exemple de code permettant de lire les CSV semble être celui de l'API Tensorflow de bas niveau. 

Et - si vous oubliez un peu de pleurnicherie - cela semble trop complexe pour le cas d'utilisation trivial consistant à envoyer des fichiers bien préparés de données de formation et de test dans une Estimator ... bien que, peut-être que ce niveau de complexité soit réellement nécessaire pour: former et tester de gros volumes de données dans Tensorflow?

Dans tous les cas, j'apprécierais vraiment un exemple d'utilisation de cette approche avec l'API de haut niveau, si c'est possible, ce dont je commence à douter.

Après avoir fouillé, j'ai réussi à trouver DNNClassifier#partial_fit , et je vais essayer de l'utiliser pour la formation. 

Des exemples d'utilisation de cette méthode me permettraient de gagner un peu de temps, mais j'espère que je trouverai le bon usage au cours des prochaines heures.

Cependant, il semble qu'il n'y ait pas de DNNClassifier#partial_evaluate correspondant ... bien que je suppose que je pourrais diviser les données de test en parties plus petites et exécuter successivement DNNClassifier#evaluate sur chaque lot, ce qui pourrait en fait être un excellent moyen de le faire, car segmenter les données de test en cohortes et ainsi obtenir une précision par cohorte.

==== Mise à jour ====

Version courte: 

  1. La recommandation de DomJack devrait être la réponse acceptée.

  2. Toutefois, les 16 Go de RAM de mon Mac sont suffisants pour qu'il conserve en mémoire l'ensemble des données d'entraînement de 709 Mo sans se planter. Ainsi, bien que j'utilise la fonctionnalité DataSets lorsque je déploierai éventuellement l'application, je ne l'utilise pas encore pour le développement local.

Version plus longue:

J'ai commencé par utiliser l'API partial_fit comme décrit ci-dessus, mais à chaque utilisation, un avertissement a été émis. 

Alors, je suis allé voir le source de la méthode ici , et j'ai découvert que son implémentation complète ressemblait à ceci:

logging.warning('The current implementation of partial_fit is not optimized'
                ' for use in a loop. Consider using fit() instead.')
return self.fit(x=x, y=y, input_fn=input_fn, steps=steps,
                batch_size=batch_size, monitors=monitors)

... qui me rappelle cette scène du guide de l'auto-stoppeur: 

Arthur Dent: Que se passe-t-il si j'appuie sur ce bouton?

Préfet de Ford: Je ne…

Arthur Dent: Oh.

Préfet de Ford: Que s'est-il passé?

Arthur Dent: Un panneau indiquant «s'il vous plaît, n'appuyez plus sur ce bouton» s'allume.

Ce qui revient à dire: partial_fit semble exister dans le seul but de vous dire de ne pas l'utiliser.

En outre, le modèle généré à l'aide de partial_fit de manière itérative sur les fichiers de formation était beaucoup plus petit que celui généré à l'aide de fit sur l'ensemble du fichier de formation, ce qui suggère fortement que seul le dernier fichier de formation partial_fit a réellement "pris".

7
aec

Découvrez le tf.data.Dataset API. Il existe plusieurs façons de créer un jeu de données. J'en exposerai trois - mais vous ne devrez en implémenter qu'un.

Je suppose que chaque ligne de vos fichiers csv contient des valeurs flottantes n_features suivies d'une valeur unique int.

Créer un tf.data.Dataset

Envelopper un générateur de python avec Dataset.from_generator

Le moyen le plus simple consiste à envelopper un générateur python natif. Je vous encourage à essayer ce premier et unique changement si vous constatez de graves problèmes de performances. 

def read_csv(filename):
    with open(filename, 'r') as f:
        for line in f.readlines():
            record = line.rstrip().split(',')
            features = [float(n) for n in record[:-1]]
            label = int(record[-1])
            yield features, label

def get_dataset():
    filename = 'my_train_dataset.csv'
    generator = lambda: read_csv(filename)
    return tf.data.Dataset.from_generator(
        generator, (tf.float32, tf.int32), ((n_features,), ()))

Cette approche est très polyvalente et vous permet de tester votre fonction de générateur (read_csv) indépendamment de TensorFlow.

Envelopper une fonction python basée sur l'index

Un des inconvénients de ce qui précède consiste à mélanger le jeu de données résultant avec un tampon de réorganisation de taille n nécessite le chargement de n exemples. Cela créera des pauses périodiques dans votre pipeline (grande n) ou entraînera un brassage potentiellement médiocre (petit n). 

def get_record(i):
    # load the ith record using standard python, return numpy arrays
    return features, labels

def get_inputs(batch_size, is_training):

    def tf_map_fn(index):
        features, labels = tf.py_func(
            get_record, (index,), (tf.float32, tf.int32), stateful=False)
        features.set_shape((n_features,))
        labels.set_shape(())
        # do data augmentation here
        return features, labels

    Epoch_size = get_Epoch_size()
    dataset = tf.data.Dataset.from_tensor_slices((tf.range(Epoch_size,))
    if is_training:
        dataset = dataset.repeat().shuffle(Epoch_size)
    dataset = dataset.map(tf_map_fn, (tf.float32, tf.int32), num_parallel_calls=8)
    dataset = dataset.batch(batch_size)
    # prefetch data to CPU while GPU processes previous batch
    dataset = dataset.prefetch(1)
    # Also possible
    # dataset = dataset.apply(
    #     tf.contrib.data.prefetch_to_device('/gpu:0'))
    features, labels = dataset.make_one_shot_iterator().get_next()
    return features, labels

En bref, nous créons un ensemble de données constitué uniquement d’indices d’enregistrement (ou de tout ID d’enregistrement de petite taille que nous pouvons charger entièrement en mémoire). Nous effectuons ensuite un brassage/une répétition des opérations sur cet ensemble de données minimal, puis map l'index des données réelles via tf.data.Dataset.map et tf.py_func. Voir les sections Using with Estimators et Testing in isolation ci-dessous pour l’utilisation. Notez que ceci nécessite que vos données soient accessibles par ligne. Vous devrez peut-être convertir de csv vers un autre format.

TextLineDataset

Vous pouvez également lire le fichier csv directement à l’aide d’un tf.data.TextLineDataset.

def get_record_defaults():
  zf = tf.zeros(shape=(1,), dtype=tf.float32)
  zi = tf.ones(shape=(1,), dtype=tf.int32)
  return [zf]*n_features + [zi]

def parse_row(tf_string):
    data = tf.decode_csv(
        tf.expand_dims(tf_string, axis=0), get_record_defaults())
    features = data[:-1]
    features = tf.stack(features, axis=-1)
    label = data[-1]
    features = tf.squeeze(features, axis=0)
    label = tf.squeeze(label, axis=0)
    return features, label

def get_dataset():
    dataset = tf.data.TextLineDataset(['data.csv'])
    return dataset.map(parse_row, num_parallel_calls=8)

La fonction parse_row est un peu compliquée car tf.decode_csv attend un lot. Vous pouvez le simplifier un peu si vous regroupez le jeu de données avant l'analyse.

def parse_batch(tf_string):
    data = tf.decode_csv(tf_string, get_record_defaults())
    features = data[:-1]
    labels = data[-1]
    features = tf.stack(features, axis=-1)
    return features, labels

def get_batched_dataset(batch_size):
    dataset = tf.data.TextLineDataset(['data.csv'])
    dataset = dataset.batch(batch_size)
    dataset = dataset.map(parse_batch)
    return dataset

TFRecordDataset

Vous pouvez également convertir les fichiers csv en fichiers TFRecord et utiliser un fichier TFRecordDataset . Il y a un tutoriel approfondi ici .

Étape 1: Convertissez les données csv en données TFRecords. Exemple de code ci-dessous (voir read_csv à partir de from_generator exemple ci-dessus).

with tf.python_io.TFRecordWriter("my_train_dataset.tfrecords") as writer:
    for features, labels in read_csv('my_train_dataset.csv'):
        example = tf.train.Example()
        example.features.feature[
            "features"].float_list.value.extend(features)
        example.features.feature[
            "label"].int64_list.value.append(label)
        writer.write(example.SerializeToString())

Cela ne doit être exécuté qu'une seule fois.

Étape 2: écrivez un jeu de données qui décode ces fichiers d’enregistrement.

def parse_function(example_proto):
    features = {
        'features': tf.FixedLenFeature((n_features,), tf.float32),
        'label': tf.FixedLenFeature((), tf.int64)
    }
    parsed_features = tf.parse_single_example(example_proto, features)
    return parsed_features['features'], parsed_features['label']

def get_dataset():
    dataset = tf.data.TFRecordDataset(['data.tfrecords'])
    dataset = dataset.map(parse_function)
    return dataset

Utilisation du jeu de données avec des estimateurs

def get_inputs(batch_size, shuffle_size):
    dataset = get_dataset()  # one of the above implementations
    dataset = dataset.shuffle(shuffle_size)
    dataset = dataset.repeat()  # repeat indefinitely
    dataset = dataset.batch(batch_size)
            # prefetch data to CPU while GPU processes previous batch
    dataset = dataset.prefetch(1)
    # Also possible
    # dataset = dataset.apply(
    #     tf.contrib.data.prefetch_to_device('/gpu:0'))
    features, label = dataset.make_one_shot_iterator().get_next()

estimator.train(lambda: get_inputs(32, 1000), max_steps=1e7)

Test du jeu de données en isolation

Je vous encourage fortement à tester votre jeu de données indépendamment de votre estimateur. En utilisant le get_inputs ci-dessus, il devrait être aussi simple que

batch_size = 4
shuffle_size = 100
features, labels = get_inputs(batch_size, shuffle_size)
with tf.Session() as sess:
    f_data, l_data = sess.run([features, labels])
print(f_data, l_data)  # or some better visualization function

Performance

En supposant que vous utilisiez un processeur graphique pour faire fonctionner votre réseau, à moins que chaque ligne de votre fichier csv soit énorme et que votre réseau soit petit, vous ne remarquerez probablement pas de différence de performances. En effet, l'implémentation Estimator force le chargement/le prétraitement des données sur la CPU, et prefetch signifie que le prochain lot peut être préparé sur la CPU alors que le lot en cours est en train de s'exercer sur le GPU. La seule exception à cette règle est si vous avez une taille de réorganisation massive sur un jeu de données contenant un grand nombre de données par enregistrement, ce qui prendra un certain temps à charger un certain nombre d'exemples avant de lancer quoi que ce soit à travers le GPU.

15
DomJack

Je suis d'accord avec DomJack sur l'utilisation de l'API Dataset, à l'exception de la nécessité de lire l'intégralité du fichier csv, puis de la convertir en TfRecord. Je propose par la présente d'utiliser TextLineDataset - une sous-classe de l'API Dataset pour charger directement des données dans un programme TensorFlow. Un tutoriel intuitif peut être trouvé ici .

Le code ci-dessous est utilisé pour illustrer le problème de classification MNIST et, espérons-le, pour répondre à la question du PO. Le fichier csv a 784 colonnes et le nombre de classes est 10. Le classificateur que j'ai utilisé dans cet exemple est un réseau de neurones à couche cachée comportant 16 unités de rapport.

Tout d’abord, chargez les bibliothèques et définissez des constantes:

# load libraries
import tensorflow as tf
import os

# some constants
n_x = 784
n_h = 16
n_y = 10

# path to the folder containing the train and test csv files
# You only need to change PATH, rest is platform independent
PATH = os.getcwd() + '/' 

# create a list of feature names
feature_names = ['pixel' + str(i) for i in range(n_x)]

Deuxièmement, nous créons une fonction d'entrée lisant un fichier à l'aide de l'API de jeu de données, puis fournissons les résultats à l'API Estimator. La valeur de retour doit être un tuple à deux éléments organisé comme suit: le premier élément doit être un dict dans lequel chaque entité en entrée est une clé, puis une liste de valeurs pour le lot d'apprentissage et le deuxième élément est une liste d'étiquettes. pour le lot de formation.

def my_input_fn(file_path, batch_size=32, buffer_size=256,\
                perform_shuffle=False, repeat_count=1):
    '''
    Args:
        - file_path: the path of the input file
        - perform_shuffle: whether the data is shuffled or not
        - repeat_count: The number of times to iterate over the records in the dataset.
                    For example, if we specify 1, then each record is read once.
                    If we specify None, iteration will continue forever.
    Output is two-element Tuple organized as follows:
        - The first element must be a dict in which each input feature is a key,
        and then a list of values for the training batch.
        - The second element is a list of labels for the training batch.
    '''
    def decode_csv(line):
        record_defaults = [[0.]]*n_x # n_x features
        record_defaults.insert(0, [0]) # the first element is the label (int)
        parsed_line = tf.decode_csv(records=line,\
                                    record_defaults=record_defaults)
        label = parsed_line[0]  # First element is the label
        del parsed_line[0]  # Delete first element
        features = parsed_line  # Everything but first elements are the features
        d = dict(Zip(feature_names, features)), label
        return d

    dataset = (tf.data.TextLineDataset(file_path)  # Read text file
               .skip(1)  # Skip header row
               .map(decode_csv))  # Transform each elem by applying decode_csv fn
    if perform_shuffle:
        # Randomizes input using a window of 256 elements (read into memory)
        dataset = dataset.shuffle(buffer_size=buffer_size)
    dataset = dataset.repeat(repeat_count)  # Repeats dataset this # times
    dataset = dataset.batch(batch_size)  # Batch size to use
    iterator = dataset.make_one_shot_iterator()
    batch_features, batch_labels = iterator.get_next()

    return batch_features, batch_labels

Ensuite, le mini-lot peut être calculé comme

next_batch = my_input_fn(file_path=PATH+'train1.csv',\
                         batch_size=batch_size,\
                         perform_shuffle=True) # return 512 random elements

Ensuite, nous définissons les colonnes de fonctionnalité sont numériques

feature_columns = [tf.feature_column.numeric_column(k) for k in feature_names]

Troisièmement, nous créons un estimateur DNNClassifier:

classifier = tf.estimator.DNNClassifier(
    feature_columns=feature_columns,  # The input features to our model
    hidden_units=[n_h],  # One layer
    n_classes=n_y,
    model_dir=None)

Enfin, le DNN est formé à l’aide du fichier test csv, tandis que l’évaluation est effectuée sur le fichier test. Veuillez modifier les repeat_count et steps pour vous assurer que la formation respecte le nombre d'époques requis dans votre code.

# train the DNN
classifier.train(
    input_fn=lambda: my_input_fn(file_path=PATH+'train1.csv',\
                                 perform_shuffle=True,\
                                 repeat_count=1),\
                                 steps=None)    

# evaluate using the test csv file
evaluate_result = classifier.evaluate(
    input_fn=lambda: my_input_fn(file_path=PATH+'test1.csv',\
                                 perform_shuffle=False))
print("Evaluation results")
for key in evaluate_result:
    print("   {}, was: {}".format(key, evaluate_result[key]))
2
Cuong