Ensembles de données

Dans de nombreux modèles d’apprentissage automatique, notamment pour l’apprentissage supervisé, les ensembles de données constituent un élément essentiel du processus de formation. Swift pour TensorFlow fournit des wrappers pour plusieurs ensembles de données courants dans le module Datasets du référentiel de modèles . Ces wrappers facilitent l'utilisation d'ensembles de données communs avec des modèles basés sur Swift et s'intègrent bien à la boucle de formation généralisée de Swift pour TensorFlow.

Wrappers d'ensemble de données fournis

Voici les wrappers d'ensembles de données actuellement fournis dans le référentiel de modèles :

Pour utiliser l'un de ces wrappers d'ensemble de données dans un projet Swift, ajoutez Datasets en tant que dépendance à votre cible Swift et importez le module :

import Datasets

La plupart des wrappers d'ensembles de données sont conçus pour produire des lots de données étiquetées mélangés de manière aléatoire. Par exemple, pour utiliser l'ensemble de données CIFAR-10, vous l'initialisez d'abord avec la taille de lot souhaitée :

let dataset = CIFAR10(batchSize: 100)

Lors de la première utilisation, les wrappers d'ensemble de données Swift pour TensorFlow téléchargeront automatiquement l'ensemble de données d'origine pour vous, extrairont et analyseront toutes les archives pertinentes, puis stockeront l'ensemble de données traité dans un répertoire de cache local de l'utilisateur. Les utilisations ultérieures du même ensemble de données seront chargées directement à partir du cache local.

Pour configurer une boucle d'entraînement manuelle impliquant cet ensemble de données, vous utiliseriez quelque chose comme ce qui suit :

for (epoch, epochBatches) in dataset.training.prefix(100).enumerated() {
  Context.local.learningPhase = .training
  ...
  for batch in epochBatches {
    let (images, labels) = (batch.data, batch.label)
    ...
  }
}

Ce qui précède configure un itérateur sur 100 époques ( .prefix(100) ) et renvoie l'index numérique de l'époque actuelle et une séquence mappée paresseusement sur des lots mélangés qui composent cette époque. Au cours de chaque époque de formation, les lots sont itérés et extraits pour traitement. Dans le cas du wrapper d'ensemble de données CIFAR10 , chaque lot est un LabeledImage , qui fournit un Tensor<Float> contenant toutes les images de ce lot et un Tensor<Int32> avec leurs étiquettes correspondantes.

Dans le cas du CIFAR-10, l'ensemble de données dans son intégralité est petit et peut être chargé en mémoire en une seule fois, mais pour d'autres ensembles de données plus volumineux, les lots sont chargés paresseusement à partir du disque et traités au moment où chaque lot est obtenu. Cela évite l’épuisement de la mémoire avec ces ensembles de données plus volumineux.

L'API Epochs

La plupart de ces wrappers d'ensembles de données sont construits sur une infrastructure partagée que nous avons appelée l' API Epochs . Epochs fournit des composants flexibles destinés à prendre en charge une grande variété de types d'ensembles de données, du texte aux images et bien plus encore.

Si vous souhaitez créer votre propre wrapper d'ensemble de données Swift, vous souhaiterez probablement utiliser l'API Epochs pour ce faire. Cependant, pour les cas courants, tels que les ensembles de données de classification d'images, nous vous recommandons fortement de partir d'un modèle basé sur l'un des wrappers d'ensemble de données existants et de le modifier pour répondre à vos besoins spécifiques.

À titre d'exemple, examinons le wrapper de l'ensemble de données CIFAR-10 et son fonctionnement. Le cœur de l'ensemble de données de formation est défini ici :

let trainingSamples = loadCIFARTrainingFiles(in: localStorageDirectory)
training = TrainingEpochs(samples: trainingSamples, batchSize: batchSize, entropy: entropy)
  .lazy.map { (batches: Batches) -> LazyMapSequence<Batches, LabeledImage> in
    return batches.lazy.map{
      makeBatch(samples: $0, mean: mean, standardDeviation: standardDeviation, device: device)
  }
}

Le résultat de la fonction loadCIFARTrainingFiles() est un tableau de tuples (data: [UInt8], label: Int32) pour chaque image de l'ensemble de données d'entraînement. Ceci est ensuite fourni à TrainingEpochs(samples:batchSize:entropy:) pour créer une séquence infinie d'époques avec des lots de batchSize . Vous pouvez fournir votre propre générateur de nombres aléatoires dans les cas où vous souhaitez un comportement de traitement par lots déterministe, mais par défaut, le SystemRandomNumberGenerator est utilisé.

À partir de là, les cartes paresseuses sur les lots aboutissent à la fonction makeBatch(samples:mean:standardDeviation:device:) . Il s'agit d'une fonction personnalisée où se trouve le pipeline de traitement d'image réel pour l'ensemble de données CIFAR-10, alors jetons un coup d'œil à cela :

fileprivate func makeBatch<BatchSamples: Collection>(
  samples: BatchSamples, mean: Tensor<Float>?, standardDeviation: Tensor<Float>?, device: Device
) -> LabeledImage where BatchSamples.Element == (data: [UInt8], label: Int32) {
  let bytes = samples.lazy.map(\.data).reduce(into: [], +=)
  let images = Tensor<UInt8>(shape: [samples.count, 3, 32, 32], scalars: bytes, on: device)

  var imageTensor = Tensor<Float>(images.transposed(permutation: [0, 2, 3, 1]))
  imageTensor /= 255.0
  if let mean = mean, let standardDeviation = standardDeviation {
    imageTensor = (imageTensor - mean) / standardDeviation
  }

  let labels = Tensor<Int32>(samples.map(\.label), on: device)
  return LabeledImage(data: imageTensor, label: labels)
}

Les deux lignes de cette fonction concatènent tous les octets data des BatchSamples entrants dans un Tensor<UInt8> qui correspond à la disposition des octets des images dans l'ensemble de données brutes CIFAR-10. Ensuite, les canaux d'image sont réorganisés pour correspondre à ceux attendus dans nos modèles de classification d'images standard et les données d'image sont refondues dans un Tensor<Float> pour la consommation du modèle.

Des paramètres de normalisation facultatifs peuvent être fournis pour ajuster davantage les valeurs des canaux d'image, un processus courant lors de la formation de nombreux modèles de classification d'images. Le paramètre de normalisation Tensor s est créé une fois lors de l'initialisation de l'ensemble de données, puis transmis à makeBatch() en tant qu'optimisation pour empêcher la création répétée de petits tenseurs temporaires avec les mêmes valeurs.

Enfin, les étiquettes entières sont placées dans un Tensor<Int32> et la paire tenseur image/étiquette renvoyée dans un LabeledImage . Un LabeledImage est un cas spécifique de LabeledData , une structure avec des données et des étiquettes conformes au protocole Collatable de l'API Eppch.

Pour plus d'exemples de l'API Epochs dans différents types d'ensembles de données, vous pouvez examiner les autres wrappers d'ensembles de données dans le référentiel de modèles.