Hé là, un autre passionné de Python! Avez-vous déjà souhaité que votre code Numpy fonctionne à la vitesse supersonique? Rencontrez Jax!. Votre nouveau meilleur ami dans votre apprentissage automatique, votre apprentissage en profondeur et votre parcours informatique numérique. Pensez-y comme numpy avec les superpuissances. Il peut gérer automatiquement les gradients, compiler votre code pour s'exécuter rapidement à l'aide de JIT et même exécuter sur GPU et TPU sans transpirer. Que vous construisiez des réseaux de neurones, que vous craquiez des données scientifiques, que vous modifiiez des modèles de transformateurs ou que vous essayiez simplement d'accélérer vos calculs, Jax est le dos. Plongeons et voyons ce qui rend Jax si spécial.
Ce guide fournit une introduction détaillée à Jax et à son écosystème.
Cet article a été publié dans le cadre du Blogathon de la science des données.
Selon la documentation officielle, Jax est une bibliothèque Python pour le calcul de la réseau orienté vers l'accélération et la transformation du programme, conçue pour l'informatique numérique haute performance et l'apprentissage automatique à grande échelle. Ainsi, Jax est essentiellement Numpy sur les stéroïdes, il combine des opérations familières de style Numpy avec une différenciation automatique et une accélération matérielle. Considérez-le comme obtenant le meilleur des trois mondes.
Ce qui distingue Jax, ce sont ses transformations. Ce sont des fonctions puissantes qui peuvent modifier votre code Python:
Voici un rapide coup d'œil:
importer jax.numpy en tant que JNP De Jax Import Grad, JIT # Définir une fonction simple @jit # accélérez avec compilation def square_sum (x): Retour JNP.SUM (JNP.Square (x)) # Obtenez automatiquement sa fonction de gradient gradient_fn = grad (square_sum) # Essayez-le x = jnp.array ([1.0, 2.0, 3.0]) print (f "Gradient: {gradient_fn (x)}")
Sortir:
Gradient: [2. 4. 6.]
Ci-dessous, nous suivrons quelques étapes pour commencer avec Jax.
La configuration de JAX est simple pour une utilisation uniquement du processeur. Vous pouvez utiliser la documentation JAX pour plus d'informations.
Créez un environnement conda pour votre projet
# Créer un conda Env pour Jax $ conda Create --name jaxdev python = 3.11 #activer l'env $ conda activer jaxdev # Créer un nom de projet JAX101 $ mkdir jax101 # Entrez dans le Dir $ cd jax101
Installation de Jax dans l'environnement nouvellement créé
# Pour le processeur uniquement Pip Installer - Pip de mise à niveau PIP Install - Opgrade "Jax" # pour GPU Pip Installer - Pip de mise à niveau PIP INSTALLANT - Upgrade "Jax [CUDA12]"
Maintenant, vous êtes prêt à plonger dans de vraies choses. Avant de vous salir les mains sur le codage pratique, apprenons de nouveaux concepts. J'expliquerai d'abord les concepts, puis nous coderons ensemble pour comprendre le point de vue pratique.
Tout d'abord, obtenez une certaine motivation, en passant, pourquoi réapprenons-nous à nouveau une nouvelle bibliothèque? Je répondrai à cette question tout au long de ce guide d'une manière étape par étape aussi simple que possible.
Considérez Jax comme un outil électrique. Alors que Numpy est comme une scie à main fiable, Jax est comme une scie électrique moderne. Cela nécessite un peu plus d'étapes et de connaissances, mais les avantages de la performance en valent la peine pour des tâches de calcul intensives.
Dans la section suivante, nous plongerons profondément dans la transformation de Jax, en commençant par la compilation JIT. Ces transformations sont ce qui donne à Jax ses superpuissances, et les comprendre est la clé pour exploiter efficacement Jax.
Les transformations de Jax sont ce qui le distingue vraiment des bibliothèques de calcul numériques telles que Numpy ou Scipy. Explorons chacun et voyons comment ils peuvent suralimenter votre code.
La compilation juste à temps optimise l'exécution du code en compilant les parties d'un programme à l'exécution plutôt qu'à l'avance.
Dans Jax, Jax.jit transforme une fonction Python en une version compilée JIT. La décoration d'une fonction avec @ jax.jit capture son graphique d'exécution, l'optimise et la compile à l'aide de XLA. La version compilée s'exécute ensuite, offrant des accéléreuses importantes, en particulier pour les appels de fonction répétés.
Voici comment vous pouvez l'essayer.
importer jax.numpy en tant que JNP De Jax Import Jit heure d'importation # Une fonction intensive sur le calcul Def Slow_Function (x): pour _ dans la gamme (1000): x = jnp.sin (x) jnp.cos (x) retour x # La même fonction avec Jit @jit def fast_function (x): pour _ dans la gamme (1000): x = jnp.sin (x) jnp.cos (x) retour x
Voici la même fonction, l'une n'est qu'un processus de compilation Python ordinaire et l'autre est utilisé comme processus de compilation JIT de Jax. Il calculera la somme des 1000 points de données des fonctions sinus et cosinus. Nous comparerons les performances en utilisant le temps.
# Comparez les performances x = jnp.arange (1000) # Jit d'échauffement fast_function (x) # premier appel compile la fonction # Comparaison du temps start = time.time () Slow_result = Slow_Function (x) print (f "sans jit: {time.time () - Démarrer: .4f} secondes") start = time.time () fast_result = fast_function (x) print (f "avec jit: {time.time () - Démarrer: .4f} secondes")
Le résultat vous étonnera. La compilation JIT est 333 fois plus rapide que la compilation normale. C'est comme comparer un vélo avec un chiron Buggati.
Sortir:
Sans jit: 0,0330 seconde Avec Jit: 0,0010 secondes
JIT peut vous donner un coup de pouce à exécution super, mais vous devez l'utiliser correctement, sinon ce sera comme conduire Bugatti sur une route de village boueuse qui n'offre pas d'installation de supercar.
JIT fonctionne mieux avec des formes et des types statiques. Évitez d'utiliser des boucles et des conditions Python qui dépendent des valeurs du tableau. JIT ne fonctionne pas avec les tableaux dynamiques.
# Bad - utilise le flux de contrôle Python @jit def bad_function (x): Si x [0]> 0: # Cela ne fonctionnera pas bien avec Jit retour x retour -x # print (bad_function (jnp.array ([1, 2, 3]))) # Bon - utilise le flux de contrôle JAX @jit def good_function (x): retourner JNP.where (x [0]> 0, x, -x) # JAX-Native Condition print (good_function (jnp.array ([1, 2, 3])))
Sortir:
Cela signifie que BAD_FUNCTION est mauvais car JIT n'était pas situé dans la valeur de X pendant le calcul.
Sortir:
[1 2 3]
La différenciation automatique, ou Autodiff, est une technique de calcul pour calculer la dérivée des fonctions avec précision et efficacité. Il joue un rôle crucial dans l'optimisation des modèles d'apprentissage automatique, en particulier dans la formation des réseaux de neurones, où les gradients sont utilisés pour mettre à jour les paramètres du modèle.
Autodiff fonctionne en appliquant la règle de la chaîne de calcul pour décomposer les fonctions complexes en plus simples, en calculant la dérivée de ces sous-fonctions, puis en combinant les résultats. Il enregistre chaque opération pendant l'exécution de la fonction pour construire un graphique de calcul, qui est ensuite utilisé pour calculer automatiquement les dérivés.
Il y a deux modes principaux de difficulté automatique:
importer jax.numpy en tant que JNP De Jax Import Grad, Value_and_grad # Définir une couche de réseau neuronal simple Def couche (params, x): poids, biais = params Retour JNP.dot (x, poids) biais # Définir une fonction de perte à valeur scalaire Def Loss_fn (params, x): sortie = couche (paramètres, x) retour jnp.sum (sortie) # réduction à un scalaire # Obtenez à la fois la sortie et le gradient couche_grate = grad (perte_fn, argnums = 0) # gradient par rapport aux paramètres couche_value_and_grad = valeur_and_grad (perte_fn, argnums = 0) # à la fois la valeur et le gradient # Exemple d'utilisation key = jax.random.prngkey (0) x = jax.random.normal (clé, (3, 4)) poids = jax.random.normal (clé, (4, 2)) biais = jax.random.normal (clé, (2,)) # Calculer les gradients Grads = couche_grade ((poids, biais), x) sortie, grads = couche_value_and_grad ((poids, biais), x) # Plusieurs dérivés sont faciles Twice_grad = Grad (Grad (JNP.Sin)) x = jnp.array (2.0) print (f "Deuxième dérivé du péché à x = 2: {twice_grad (x)}")
Sortir:
Deuxième dérivés du péché à x = 2: -0,9092974066734314
Dans JAX, `VMAP` est une fonction puissante qui vectorise automatiquement les calculs, vous permettant d'appliquer une fonction sur des lots de données sans écrire manuellement les boucles. Il mappe une fonction sur un axe de tableau (ou plusieurs axes) et l'évalue efficacement en parallèle, ce qui peut entraîner des améliorations de performances significatives.
La fonction VMAP automatise le processus d'application d'une fonction à chaque élément le long d'un axe spécifié d'un tableau d'entrée tout en préservant l'efficacité du calcul. Il transforme la fonction donnée pour accepter les entrées par lots et exécuter le calcul de manière vectorisée.
Au lieu d'utiliser des boucles explicites, VMAP permet d'effectuer des opérations en parallèle en vectorisant sur un axe d'entrée. Cela tire parti de la capacité du matériel à effectuer des opérations SIMD (instructions uniques, données multiples), ce qui peut entraîner des accélération substantielles.
importer jax.numpy en tant que JNP De Jax Import VMAP # Une fonction qui fonctionne sur des entrées uniques def single_input_fn (x): retour jnp.sin (x) jnp.cos (x) # Vectorisez-le pour travailler sur des lots batch_fn = vmap (single_input_fn) # Comparez les performances x = jnp.arange (1000) # Sans VMAP (en utilisant une compréhension de la liste) result1 = jnp.array ([single_input_fn (xi) pour xi dans x]) # Avec VMAP result2 = Batch_fn (x) # beaucoup plus rapide! # Vectorisation de plusieurs arguments def deux_input_fn (x, y): retour x * jnp.sin (y) # Vectoriser sur les deux entrées Vectrized_fn = vmap (Two_input_fn, in_axes = (0, 0)) # Ou vectoriser sur la première entrée partiellement_vectorized_fn = vmap (deux_input_fn, in_axes = (0, aucun)) # imprimer imprimer (résultat1.shape) imprimer (result2.shape) imprimer (partiellement_vectorized_fn (x, y) .shape)
Sortir:
(1000,) (1000,) (1000,3)
Jax fournit un support complet pour les opérations matricielles et l'algèbre linéaire, ce qui le rend adapté aux tâches de calcul scientifique, d'apprentissage automatique et d'optimisation numérique. Les capacités d'algèbre linéaire de Jax sont similaires à celles trouvées dans des bibliothèques comme Numpy, mais avec des fonctionnalités supplémentaires telles que la différenciation automatique et la compilation juste en temps pour des performances optimisées.
Ces opérations sont effectuées des matrices d'élément de la même forme.
# 1 Ajout de matrice et soustraction: importer jax.numpy en tant que JNP A = jnp.array ([[1, 2], [3, 4]]) B = jnp.array ([[5, 6], [7, 8]]) # Ajout de matrice C = ab # Soustraction matricielle D = a - b print (f "matrice a: \ n {a}") Print ("==========================") print (f "matrice b: \ n {b}") Print ("==========================") print (f "ADITION MATRIX DE L'AB: \ N {C}") Print ("==========================") Print (F "substraction matricielle d'Ab: \ n {d}")
Sortir:
JAX prend en charge la multiplication par élément et la multiplication de la matrice basée sur les produits DOR.
# Multiplication sur les éléments E = a * b Multiplication de la matrice (produit DOT) F = jnp.dot (a, b) print (f "matrice a: \ n {a}") Print ("==========================") print (f "matrice b: \ n {b}") Print ("==========================") print (f "Multiplication par élément de a * b: \ n {e}") Print ("==========================") print (f "Multiplication matricielle de a * b: \ n {f}")
Sortir:
La transposition d'une matrice peut être obtenue en utilisant `JNP.TRANSPOSE ()»
# Transsposition matricielle G = JNP.TRANSPOSE (A) print (f "matrice a: \ n {a}") Print ("==========================") print (f "Transposition matricielle de a: \ n {g}")
Sortir:
Jax fournit une fonction pour l'inversion de la matrice en utilisant `jnp.linalg.inv ()`
# Inversion matrique H = jnp.linalg.inv (a) print (f "matrice a: \ n {a}") Print ("==========================") print (f "Inversion matricielle de a: \ n {h}")
Sortir:
Le déterminant d'une matrice peut être calculé à l'aide de `jnp.linalg.det ()`.
# Matrix déterminant DET_A = JNP.LINALG.DET (A) print (f "matrice a: \ n {a}") Print ("==========================") print (f "Matrix déterminant de a: \ n {det_a}")
Sortir:
Vous pouvez calculer les valeurs propres et les vecteurs propres d'une matrice en utilisant `JNP.Linalg.Eigh ()`
# Valeurs propres et vecteurs propres importer jax.numpy en tant que JNP A = jnp.array ([[1, 2], [3, 4]]) valeurs propres, vecteurs propres = Jnp.Linalg.Eigh (a) print (f "matrice a: \ n {a}") Print ("==========================") print (f "valeurs propres de a: \ n {valeurs propres}") Print ("==========================") print (f "vecteurs propres de a: \ n {vecteurs propres}")
Sortir:
SVD est pris en charge via `jnp.linalg.svd`, utile dans la réduction de la dimensionnalité et la factorisation de la matrice.
# Décomposition de la valeur singulière (SVD) importer jax.numpy en tant que JNP A = jnp.array ([[1, 2], [3, 4]]) U, s, v = jnp.linalg.svd (a) print (f "matrice a: \ n {a}") Print ("==========================") print (f "matrice u: \ n {u}") Print ("==========================") print (f "matrice s: \ n {s}") Print ("==========================") print (f "matrice v: \ n {v}")
Sortir:
Pour résoudre un système d'équation linéaire AX = B, nous utilisons `jnp.linalg.solve () ', où A est une matrice carrée et B est un vecteur ou une matrice du même nombre de lignes.
# Résolution du système d'équations linéaires importer jax.numpy en tant que JNP A = jnp.array ([[2.0, 1.0], [1.0, 3.0]]) b = jnp.array ([5.0, 6.0]) x = jnp.linalg.solve (a, b) print (f "valeur de x: {x}")
Sortir:
Valeur de x: [1.8 1.4]
En utilisant la différenciation automatique de Jax, vous pouvez calculer le gradient d'une fonction scalaire par rapport à une matrice.
Nous calculerons le gradient de la fonction et les valeurs ci-dessous de x
Fonction
# Calcul le gradient d'une fonction matricielle Importer Jax importer jax.numpy en tant que JNP Def Matrix_Function (x): Retour JNP.SUM (JNP.SIN (X) X ** 2) # Calculez le diplômé de la fonction grad_f = jax.grad (matrix_function) X = jnp.array ([[1.0, 2.0], [3.0, 4.0]]) gradient = grad_f (x) print (f "matrice x: \ n {x}") Print ("==========================") print (f "Gradient de matrix_function: \ n {gradient}")
Sortir:
Cette fonction la plus utile de Jax utilisée dans le calcul numérique, l'apprentissage automatique et le calcul de la physique. Il reste beaucoup d'autres à explorer.
Les puissantes bibliothèques de l'informatique scientifique de Jax, Jax est la meilleure pour l'informatique scientifique pour ses fonctionnalités avancées telles que la compilation JIT, la différenciation automatique, la vectorisation, la parallélisation et l'accélération GPU-TPU. La capacité de Jax à prendre en charge l'informatique haute performance le rend adapté à un large éventail d'applications scientifiques, y compris les simulations de physique, l'apprentissage automatique, l'optimisation et l'analyse numérique.
Nous explorerons un problème d'optimisation dans cette section.
Passons par les étapes des problèmes d'optimisation ci-dessous:
# Définissez une fonction à minimiser (par exemple, fonction Rosenbrock) @jit Def Rosenbrock (X): Sum de retour (100.0 * (x [1:] - x [: - 1] ** 2.0) ** 2.0 (1 - x [: - 1]) ** 2.0)
Ici, la fonction Rosenbrock est définie, ce qui est un problème de test courant dans l'optimisation. La fonction prend un tableau X en entrée et calcule un VALIE qui représente à quel point X est le long du minimum global de la fonction. Le décorateur @jit est utilisé pour permettre la compilation Jut-in-Time, qui accélère le calcul en compilant la fonction pour s'exécuter efficacement sur les CPU et les GPU.
# Optimisation de descente de gradient @jit Def Gradient_Descent_step (X, Learning_Rate): retour x - Learning_rate * Grad (Rosenbrock) (x)
Cette fonction effectue une seule étape de l'optimisation de descente du gradient. Le gradient de la fonction Rosenbrock est calculé en utilisant Grad (Rosenbrock) (x), qui fournit la dérivée par rapport à x. La nouvelle valeur de x est mise à jour par soustraction Le gradient mis à l'échelle par un apprentissage_rate.Le @jit fait de même qu'auparavant.
# Optimiser x = jnp.array ([0,0, 0,0]) # point de départ apprentissage_rate = 0,001 Pour I In Range (2000): x = gradient_descent_step (x, apprentissage_rate) Si je% 100 == 0: print (f "Step {i}, valeur: {rosenbrock (x) :. 4f}")
La boucle d'optimisation initialise le point de départ X et effectue 1000 itérations de descente de gradient. Dans chaque itération, la fonction gradient_descent_step se met à jour basée sur le gradient actuel. Toutes les 100 étapes, le numéro d'étape actuel et la valeur de la fonction Rosenbrock à X sont imprimés, fournissant la progression de l'optimisation.
Sortir:
Nous simulerons un système physique le mouvement d'un oscillateur harmonique amorti, qui modélise des choses comme un système de ressort de masse avec des frottements, des amortisseurs dans les véhicules ou l'oscillation dans les circuits électriques. N'est-ce pas sympa? Faisons-le.
Importer Jax importer jax.numpy en tant que JNP # Définir les paramètres masse = 1,0 # masse de l'objet (kg) amortissement = 0,1 # Coefficient d'amortissement (kg / s) Spring_Constant = 1,0 # constante de printemps (n / m) # Définir le pas de temps et le temps total dt = 0,01 # pas de temps (s) num_steps = 3000 # Nombre d'étapes
La masse, le coefficient d'amortissement et la constante de ressort sont définis. Ceux-ci déterminent les propriétés physiques de l'oscillateur harmonique amorti.
# Définir le système d'ODE def amormed_harmonic_oscillateur (État, t): "" "Calculez les dérivés d'un oscillateur harmonique amorti. État: tableau contenant la position et la vitesse [x, v] T: Temps (non utilisé dans ce système autonome) "" " x, v = état dxdt = v dvdt = -damping / masse * v - printemps_constant / masse * x return jnp.array ([dxdt, dvdt])
La fonction d'oscillateur harmonique amorti définit les dérivés de la position et de la vitesse de l'oscillateur, représentant le système dynamique.
# Résoudre l'ODE en utilisant la méthode d'Euler Def Euler_step (State, T, DT): "" "Effectuez une étape de la méthode d'Euler." "" dérivés = amormed_harmonic_oscillator (État, t) Dérivés de retour d'État * dt
Une méthode numérique simple est utilisée pour résoudre l'ODE. Il se rapproche de l'état au prochain pas sur la base de l'état actuel et du dérivé.
# État initial: [position, vitesse] initial_state = jnp.array ([1.0, 0,0]) # Commencez par la masse à x = 1, v = 0 # Time Evolution states = [initial_state] temps = 0,0 pour étape dans la plage (num_steps): next_state = euler_step (états [-1], temps, dt) states.append (next_state) temps = dt # Convertir la liste des états en un tableau JAX pour analyse États = JNP.stack (États)
La boucle itera via les étapes de temps spécifiées, mettant à jour l'état à chaque étape à l'aide de la méthode d'Euler.
Sortir:
Enfin, nous pouvons tracer les résultats pour visualiser le comportement de l'oscillateur harmonique amorti.
# Traçant les résultats Importer Matplotlib.pyplot en tant que plt plt.style.use ("ggplot") Positions = États [:, 0] vitesses = états [:, 1] Time_points = jnp.arange (0, (num_steps 1) * dt, dt) Plt.Figure (FigSize = (12, 6)) plt.subplot (2, 1, 1) plt.plot (time_points, positions, label = "position") plt.xLabel ("temps (s)") plt.ylabel ("position (m)") plt.legend () plt.subplot (2, 1, 2) plt.plot (time_points, vitesses, label = "Velocity", color = "orange") plt.xLabel ("temps (s)") plt.ylabel ("Velocity (m / s)") plt.legend () plt.tight_layout () plt.show ()
Sortir:
Je sais que vous êtes impatient de voir comment le réseau neuronal peut être construit avec Jax. Alors, plongeons-y profondément.
Ici, vous pouvez voir que les valeurs ont été minimisées progressivement.
Jax est une bibliothèque puissante qui combine un calcul numérique haute performance avec la facilité d'utilisation de la syntaxe de type Numpy. Cette section vous guidera tout au long du processus de construction d'un réseau neuronal à l'aide de JAX, en tirant parti de ses fonctionnalités avancées pour la différenciation automatique et la compilation juste à temps pour optimiser les performances.
Avant de plonger dans la construction de notre réseau de neurones, nous devons importer les bibliothèques nécessaires. Jax fournit une suite d'outils pour créer des calculs numériques efficaces, tandis que des bibliothèques supplémentaires aideront à l'optimisation et à la visualisation de nos résultats.
Importer Jax importer jax.numpy en tant que JNP De Jax Import Grad, JIT de Jax.Random Import Prngkey, normal Importer la bibliothèque d'optimisation de l'Optax # Jax Importer Matplotlib.pyplot en tant que plt
La création de couches de modèle efficaces est cruciale pour définir l'architecture de notre réseau neuronal. Dans cette étape, nous allons initialiser les paramètres de nos couches denses, garantissant que notre modèle commence par des poids et des biais bien définis pour un apprentissage efficace.
def init_layer_params (key, n_in, n_out): "" "Initialiser les paramètres pour une seule couche dense" "" key_w, key_b = jax.random.split (key) # He initialisation w = normal (key_w, (n_in, n_out)) * jnp.sqrt (2.0 / n_in) b = normal (key_b, (n_out,)) * 0.1 retour (w, b) def relu (x): "" "Fonction d'activation de relu" "" retour jnp.maximum (0, x)
La passe avant est la pierre angulaire d'un réseau neuronal, car il dicte comment les données d'entrée circulent dans le réseau pour produire une sortie. Ici, nous définirons une méthode pour calculer la sortie de notre modèle en appliquant des transformations aux données d'entrée via les couches initialisées.
DEFFANT (params, x): "" "Passe avant pour un réseau neuronal à deux couches" "" (w1, b1), (w2, b2) = params # Première couche H1 = relu (jnp.dot (x, w1) b1) # Couche de sortie Logits = jnp.dot (H1, W2) B2 Logits de retour
Une fonction de perte bien définie est essentielle pour guider la formation de notre modèle. Dans cette étape, nous implémenterons la fonction moyenne de perte d'erreur carré (MSE), qui mesure la façon dont les sorties prédites correspondent aux valeurs cibles, permettant au modèle d'apprendre efficacement.
Def Loss_fn (Params, X, Y): "" "Moyenne perte d'erreur carré" "" pred = en avant (params, x) retour jnp.mean ((pred - y) ** 2)
Avec notre architecture de modèle et notre fonction de perte définie, nous nous tournons maintenant vers l'initialisation du modèle. Cette étape consiste à configurer les paramètres de notre réseau neuronal, garantissant que chaque couche est prête à commencer le processus de formation avec des poids et des biais à l'échelle aléatoire mais correctement.
def init_model (rng_key, input_dim, HIDDEN_DIM, output_dim): key1, key2 = jax.random.split (rng_key) params = [ init_layer_params (key1, input_dim, HIDDEN_DIM), init_layer_params (key2, HIDDEN_DIM, output_dim), ]] paramètres de retour
La formation d'un réseau neuronal implique des mises à jour itératives de ses paramètres en fonction des gradients calculés de la fonction de perte. Dans cette étape, nous implémenterons une fonction de formation qui applique efficacement ces mises à jour, permettant à notre modèle d'apprendre des données sur plusieurs époques.
@jit def Train_step (params, opt_state, x_batch, y_batch): perte, grads = jax.value_and_grad (Loss_fn) (params, x_batch, y_batch) Mises à jour, opt_state = optimizer.update (Grads, Opt_State) params = optax.apply_updates (params, mises à jour) Retour params, opt_state, perte
Pour former efficacement notre modèle, nous devons générer des données appropriées et implémenter une boucle de formation. Cette section couvrira comment créer des données synthétiques pour notre exemple et comment gérer le processus de formation sur plusieurs lots et époques.
# Générez des exemples de données clé = prngkey (0) x_data = normal (clé, (1000, 10)) # 1000 échantillons, 10 fonctionnalités y_data = jnp.sum (x_data ** 2, axe = 1, keepdims = true) # fonction non linéaire simple # Initialiser le modèle et l'optimiseur params = init_model (key, input_dim = 10, Hidden_dim = 32, output_dim = 1) Optimizer = optax.adam (apprentissage_rate = 0,001) opt_state = optimizer.init (params) # Boucle de formation Batch_size = 32 num_epochs = 100 num_batches = x_data.shape [0] // Batch_size # Arrays pour stocker des valeurs d'époque et de perte epoch_array = [] pertes_array = [] pour l'époque dans la gamme (num_epochs): epoch_loss = 0,0 pour un lot dans la plage (num_batches): idx = jax.random.permutation (key, batch_size) x_batch = x_data [idx] y_batch = y_data [idx] Params, Opt_State, Loss = Train_step (params, opt_state, x_batch, y_batch) epoch_loss = perte # Stockez la perte moyenne de l'époque avg_loss = epoch_loss / num_batches epoch_array.append (Epoch) pertes_array.append (avg_loss) Si l'époque% 10 == 0: print (f "epoch {epoch}, perte: {avg_loss: .4f}")
La visualisation des résultats de la formation est essentielle pour comprendre les performances de notre réseau neuronal. Dans cette étape, nous allons tracer la perte de formation sur les époques pour observer dans quelle mesure le modèle apprend et pour identifier tout problème potentiel dans le processus de formation.
# Tracer les résultats plt.plot (epoch_array, loss_array, label = "Training Loss") plt.xLabel ("époque") plt.ylabel ("perte") Plt.Title ("Perte d'entraînement sur les époques") plt.legend () plt.show ()
Ces exemples montrent comment Jax combine des performances élevées avec un code propre et lisible. Le style de programmation fonctionnelle encouragé par Jax facilite la composition des opérations et applique des transformations.
Sortir:
Parcelle:
Ces exemples montrent comment Jax combine des performances élevées avec un code propre et lisible. Le style de programmation fonctionnelle encouragé par Jax facilite la composition des opérations et applique des transformations.
In building neural networks, adhering to best practices can significantly enhance performance and maintainability. This section will discuss various strategies and tips for optimizing your code and improving the overall efficiency of your JAX-based models.
Optimizing performance is essential when working with JAX, as it enables us to fully leverage its capabilities. Here, we will explore different techniques for improving the efficiency of our JAX functions, ensuring that our models run as quickly as possible without sacrificing readability.
Just-In-Time (JIT) compilation is one of the standout features of JAX, enabling faster execution by compiling functions at runtime. This section will outline best practices for effectively using JIT compilation, helping you avoid common pitfalls and maximize the performance of your code.
import jax import jax.numpy as jnp from jax import jit from jax import lax # BAD: Dynamic Python control flow inside JIT @jit def bad_function(x, n): for i in range(n): # Python loop - will be unrolled x = x 1 return x print("===========================") # print(bad_function(1, 1000)) # does not work
This function uses a standard Python loop to iterate n times, incrementing the of x by 1 on each iteration. When compiled with jit, JAX unrolls the loop, which can be inefficient, especially for large n. This approach does not fully leverage JAX's capabilities for performance.
# GOOD: Use JAX-native operations @jit def good_function(x, n): return xn # Vectorized operation print("===========================") print(good_function(1, 1000))
This function does the same operation, but it uses a vectorized operation (xn) instead of a loop. This approach is much more efficient because JAX can better optimize the computation when expressed as a single vectorized operation.
# BETTER: Use scan for loops @jit def best_function(x, n): def body_fun(i, val): return val 1 return lax.fori_loop(0, n, body_fun, x) print("===========================") print(best_function(1, 1000))
This approach uses `jax.lax.fori_loop`, which is a JAX-native way to implement loops efficiently. The `lax.fori_loop` performs the same increment operation as the previous function, but it does so using a compiled loop structure. The body_fn function defines the operation for each iteration, and `lax.fori_loop` executes it from o to n. This method is more efficient than unrolling loops and is especially suitable for cases where the number of iterations isn't known ahead of time.
Sortir :
=========================== =========================== 1001 =========================== 1001
The code demonstrates different approaches to handling loops and control flow within JAX's jit-complied functions.
Efficient memory management is crucial in any computational framework, especially when dealing with large datasets or complex models. This section will discuss common pitfalls in memory allocation and provide strategies for optimizing memory usage in JAX.
# BAD: Creating large temporary arrays @jit def inefficient_function(x): temp1 = jnp.power(x, 2) # Temporary array temp2 = jnp.sin(temp1) # Another temporary return jnp.sum(temp2)
inefficient_function(x): This function creates multiple intermediate arrays, temp1, temp1 and finally the sum of the elements in temp2. Creating these temporary arrays can be inefficient because each step allocates memory and incurs computational overhead, leading to slower execution and higher memory usage.
# GOOD: Combining operations @jit def efficient_function(x): return jnp.sum(jnp.sin(jnp.power(x, 2))) # Single operation
This version combines all operations into a single line of code. It computes the sine of squared elements of x directly and sums the results. By combining the operation, it avoids creating intermediate arrays, reducing memory footprints and improving performance.
x = jnp.array([1, 2, 3]) print(x) print(inefficient_function(x)) print(efficient_function(x))
Sortir:
[1 2 3] 0.49678695 0.49678695
The efficient version leverages JAX's ability to optimize the computation graph, making the code faster and more memory-efficient by minimizing temporary array creation.
Debugging is an essential part of the development process, especially in complex numerical computations. In this section, we will discuss effective debugging strategies specific to JAX, enabling you to identify and resolve issues quickly.
The code shows techniques for debugging within JAX, particularly when using JIT-compiled functions.
import jax.numpy as jnp from jax import debug @jit def debug_function(x): # Use debug.print instead of print inside JIT debug.print("Shape of x: {}", x.shape) y = jnp.sum(x) debug.print("Sum: {}", y) return y
# For more complex debugging, break out of JIT def debug_values(x): print("Input:", x) result = debug_function(x) print("Output:", result) return result
Sortir:
print("===========================") print(debug_function(jnp.array([1, 2, 3]))) print("===========================") print(debug_values(jnp.array([1, 2, 3])))
This approach allows for a combination of in-JIT debugging with debug.print() and more detailed debugging outside of JIT using standard Python print statements.
Finally, we will explore common patterns and idioms in JAX that can help streamline your coding process and improve efficiency. Familiarizing yourself with these practices will aid in developing more robust and performant JAX applications.
# 1. Device Memory Management def process_large_data(data): # Process in chunks to manage memory chunk_size = 100 results = [] for i in range(0, len(data), chunk_size): chunk = data[i : i chunk_size] chunk_result = jit(process_chunk)(chunk) results.append(chunk_result) return jnp.concatenate(results) def process_chunk(chunk): chunk_temp = jnp.sqrt(chunk) return chunk_temp
This function processes large datasets in chunks to avoid overwhelming device memory.
It sets chunk_size to 100 and iterates over the data increments of the chunk size, processing each chunk separately.
For each chunk, the function uses jit(process_chunk) to JIT-compile the processing operation, which improves performance by compiling it ahead of time.
The result of each chunk is concatenated into a single array using jnp.concatenated(result) to form a single list.
Sortir:
print("===========================") data = jnp.arange(10000) print(data.shape) print("===========================") print(data) print("===========================") print(process_large_data(data))
The function create_traing_state() demonstrates managing random number generators (RNGs) in JAX, which is essential for reproducibility and consistent results.
# 2. Handling Random Seeds def create_training_state(rng): # Split RNG for different uses rng, init_rng = jax.random.split(rng) params = init_network(init_rng) return params, rng # Return new RNG for next use
It starts with an initial RNG (rng) and splits it into two new RNGs using jax.random.split(). Split RNGs perform different tasks: `init_rng` initializes network parameters, and the updated RNG returns for subsequent operations.
The function returns both the initialized network parameters and the new RNG for further use, ensuring proper handling of random states across different steps.
Now test the code using mock data
def init_network(rng): # Initialize network parameters retour { "w1": jax.random.normal(rng, (784, 256)), "b1": jax.random.normal(rng, (256,)), "w2": jax.random.normal(rng, (256, 10)), "b2": jax.random.normal(rng, (10,)), } print("===========================") key = jax.random.PRNGKey(0) params, rng = create_training_state(key) print(f"Random number generator: {rng}") print(params.keys()) print("===========================") print("===========================") print(f"Network parameters shape: {params['w1'].shape}") print("===========================") print(f"Network parameters shape: {params['b1'].shape}") print("===========================") print(f"Network parameters shape: {params['w2'].shape}") print("===========================") print(f"Network parameters shape: {params['b2'].shape}") print("===========================") print(f"Network parameters: {params}")
Sortir:
def g(x, n): i = 0 while i <p> <strong>Sortir:</strong></p><pre class="brush:php;toolbar:false"> 30
You can use a static argument if JIT compiles the function with the same arguments each time. This can be useful for the performance optimization of JAX functions.
from functools import partial @partial(jax.jit, static_argnames=["n"]) def g_jit_decorated(x, n): i = 0 while i <p>If You want to use static arguments in JIT as a decorator you can use jit inside of functools. partial() function.</p><p> <strong>Sortir:</strong></p><pre class="brush:php;toolbar:false"> 30
Now, we have learned and dived deep into many exciting concepts and tricks in JAX and overall programming style.
All code used in this article is here
JAX is a powerful tool that provides a wide range of capabilities for machine learning, Deep Learning, and scientific computing. Start with basics, experimenting, and get help from JAX's beautiful documentation and community. There are so many things to learn and it will not be learned by just reading others' code you have to do it on your own. So, start creating a small project today in JAX. The key is to Keep Going, learn on the way.
A. Although JAX feels like NumPy, it adds automatic differentiation, JIT compilation, and GPU/TPU support.
Q2. Do I need a GPU to use JAX?A. In a single word big NO, though having a GPU can significantly speed up computation for larger data.
Q3. Is JAX a good alternative to NumPy?A. Yes, You can use JAX as an alternative to NumPy, though JAX's APIs look familiar to NumPy JAX is more powerful if you use JAX's features well.
Q4. Can I use my existing NumPy code with JAX?A. Most NumPy code can be adapted to JAX with minimal changes. Usually just changing import numpy as np to import jax.numpy as jnp.
Q5. Is JAX harder to learn than NumPy?A. The basics are just as easy as NumPy! Tell me one thing, will you find it hard after reading the above article and hands-on? I answered it for you. YES hard. Every framework, language, libraries is hard not because it is hard by design but because we don't give much time to explore it. Give it time to get your hand dirty it will be easier day by day.
Les médias présentés dans cet article ne sont pas détenus par l'analytique vidhya et sont utilisés à la discrétion de l'auteur.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!