Comment faire FastAI encore plus vite

Accélérez le prétraitement en utilisant la fonction «parallèle» intégrée de Fastai

La puissance du traitement en parallèle. Photo par Marc-Olivier Jodoin sur Unsplash

Je participe à ma première compétition Kaggle, celle déjà terminée
Défi de reconnaissance vocale du flux tensoriel, qui implique le traitement
clips audio bruts d’une seconde pour comprendre / prédire quel mot est dit.

Ma méthode consiste à convertir l'audio brut en spectrogrammes avant de procéder à la classification des images. J'avais construit un modèle fonctionnel et précis et j'étais ravi de l'essayer sur le jeu de tests et de faire ma première soumission, lorsque j'ai vu que le jeu de tests contenait plus de 150 000 fichiers wav. Je produis actuellement environ 4 spectrogrammes / seconde au format 224x224. C’est environ 10 heures, il doit y avoir un meilleur moyen.

Immédiatement, le multitraitement et le multithreading me viennent à l’esprit, mais je ne sais pas comment les réaliser, ils ont l’air effrayant et ils nécessitent davantage d’apprentissage lorsque mon assiette est déjà bien remplie. Heureusement, fastai a une fonctionnalité «parallèle» que Jeremy mentionne avec désinvolture dans la leçon 7. La première étape consiste toujours à vérifier la documentation.

doc (parallèle)
Documentation dans l'ordinateur (de base) pour fastai.core.parallel

Show in docs parallel [source] [test]

Génial, ça a l'air simple comme on peut. Je passe une fonction et une collection d'arguments à cette fonction pour qu'elle soit exécutée en parallèle, et fastai gère le reste.

Un jouet exemple de parallèle

La fonction ci-dessous prend un numéro et l’imprime carré.

def print_square (num):
    imprimer (nombre ** 2)

Ensuite, nous pouvons générer une liste contenant des listes de nombres que nous voulons obtenir en utilisant une compréhension de liste.

num_list = [x pour x dans l'intervalle (10)]
parallèle (print_square, num_list)

Exécutez ce code et vous verrez ce qui suit:

On dirait que cela a très bien fonctionné, mais où sont nos carrés? Nous étions censés les imprimer. Si vous lisez plus attentivement ici, vous verrez ce qui se passe:

Documentation complète pour fastai.core.parallel

“Func doit accepter à la fois la valeur et l'index de chaque élément arr”

La fonction que vous transmettez doit être d'un type spécial et n'accepter que deux arguments:

  1. Une valeur (c'est ce que contient contient et est l'argument normal de votre fonction)
  2. L'index * de la valeur dans arr (note: votre fonction n'a pas besoin de faire quoi que ce soit avec l'index, elle doit juste l'avoir dans la définition de la fonction)
* Sylvain Gugger m'a informé sur les forums fastai que l'index est nécessaire pour que verify_images fonctionne. Par ailleurs, parallèle est censé être une fonction pratique pour une utilisation interne. Ainsi, si vous êtes plus avancé, vous pouvez implémenter votre propre version à l'aide de ProcessPoolExecutor.

Réécrivons notre fonction pour accepter un index. Encore une fois, notre fonction n'a rien à faire avec l'index et nous pourrions même le remplacer par _ dans la définition de la fonction.

def print_square (num, index):
    print (num ** 2)

Et maintenant, nous essayons à nouveau d'appeler en parallèle…

Ça marche! Si vous avez une fonction simple qui prend un argument, vous avez terminé. Vous savez maintenant comment utiliser la fonction parallèle de fastai pour le faire 2 à 10 fois plus rapidement! * Il vous suffit de modifier les paramètres pour accepter un index et de passer votre fonction à une parallèle avec une collection d’arguments.
* Ceci est un exemple de jouet, et il est environ 1000x plus lent à utiliser le parallèle, voir ci-dessous pour un exemple réel avec de vrais repères

À quoi cela ressemble-t-il dans la pratique, avec un exemple plus réaliste…

Voici mon code actuel pour générer et enregistrer des spectrogrammes.
Code original fourni par John Hartquist et Kajetan Olszewski
TLDR: Lire le fichier wav dans src_path / fname, créer un spectrogramme, enregistrer dans dst_path.

def gen_spec (fname, src_path, dst_path):
    y, sr = librosa.load (src_path / fname)
    S = librosa.feature.melspectrogram (y, sr = sr, n_fft = 1024,
                                       longueur de hop = 512, n_mels = 128,
                                      puissance = 1,0, fmin = 20, fmax = 8000)
    plt.figure (figsize = (2.24, 2.24))
    pylab.axis ('off')
    pylab.axes ([0., 0., 1., 1.], frameon = False, xticks = [], yticks = [])
    librosa.display.specshow (librosa.power_to_db (S, ref = np.max),
                             y_axis = 'mel', x_axis = 'time')
    save_path = f '{dst_path / fname} .png'
    pylab.savefig (save_path, bbox_inches = None, pad_inches = 0, dpi = 100)
    pylab.close ()

Avant d’aller plus loin, regardons le code source pour le parallèle.

Code source pour fastai.core.parallel, pas aussi effrayant qu'il en a l'air

Cela semble intimidant, mais tout ce qui se passe réellement ici, c'est que nous obtenons chaque valeur de notre collection d'arguments et que nous la stockons dans o, ainsi que l'index de ladite valeur dans i, puis en appelant func (o, i).
Pour nous, cela signifie que parallel va appeler gen_spec (fname, index) pour chaque fname de la collection (dans notre cas, une liste) que nous soumettons, et qu'il gérera le traitement parallèle pour nous, ce qui accélérera considérablement le traitement.

Mais que faisons-nous si notre fonction accepte plus d'un argument?

Comme vous pouvez le constater, notre fonction gen_spec prend 3 arguments et Parallèle attend une fonction qui en prend deux. La solution dépend si nos arguments supplémentaires sont toujours les mêmes, comme un chemin de fichier ou une constante, ou s'ils vont varier.

A. Si les arguments supplémentaires sont fixes / statiques, créez une nouvelle fonction avec des valeurs par défaut ou utilisez partial de python pour créer une fonction qui convient au modèle de parallèles. Je préfère utiliser partial, c’est ce que je vais démontrer ci-dessous.

B. Si vous avez plusieurs arguments qui vont changer avec chaque appel de fonction, transmettez-les comme un tuple d'arguments, puis décompressez-les.

Solution A: Tous les 150 000 de mes fichiers wav sont situés dans le même chemin src_path, et je vais exporter tous les spectrogrammes dans le même chemin dst_path. Le seul argument qui change est donc fname. C'est l'endroit idéal pour utiliser une partielle

Puisque parallel ne nomme pas explicitement index dans l’appel de la fonction, il doit toujours être le deuxième argument de notre définition. Voyons ça réparer. Maintenant nous avons:

def gen_spec (fname, index, src_path, dst_path):

Ensuite, nous créons une nouvelle fonction gen_spec_partial en passant par nos chemins statiques

gen_spec_partial = partial (gen_spec, src_path = path_audio,
                           dst_path = path_spectrogram)

C’est tout, nous avons terminé. Créons 1000 spectrogrammes en utilisant à la fois gen_spec_partial et en parallèle et comparons le temps qu’il prend.

296 secondes sans parallèle, 104 secondes avec parallèle, presque 3 fois plus rapide.

Solution B: Maintenant, pour notre dernier cas, que faisons-nous si nos arguments supplémentaires ne sont pas statiques? Nous réécrivons notre fonction pour accepter un tuple d'arguments et un index, puis nous passons une collection de n-uplets contenant les arguments. Pour notre exemple de spectrogramme, cela ressemble à ceci:

def gen_spec_parallel_tuple (arg_tuple, index):
    fname, src_path, dst_path = arg_tuple
    # le code restant est le même et a été omis

Nous regroupons ensuite tous les arguments que nous voulons transmettre dans un tuple de taille appropriée, puis passons gen_spec_parallel_tuple et notre argument arg_tuple_list au parallèle.

Ça marche! Vous savez maintenant comment utiliser des fonctions avec un nombre arbitraire d'arguments en parallèle pour accélérer votre prétraitement et consacrer plus de temps à la formation.