Utilisation de SciPy pour l'optimisation – Real Python

By | janvier 20, 2020

Expert Python

Lorsque vous souhaitez effectuer des travaux scientifiques en Python, la première bibliothèque vers laquelle vous pouvez vous tourner est SciPy. Comme vous le verrez dans ce didacticiel, SciPy n'est pas seulement une bibliothèque, mais un tout écosystème de bibliothèques qui travaillent ensemble pour vous aider à accomplir des tâches scientifiques complexes de manière rapide et fiable.

Dans ce didacticiel, vous allez apprendre à:

  • Trouver des informations sur tout ce que vous pouvez faire avec SciPy
  • Installer SciPy sur votre ordinateur
  • Utilisation SciPy pour regrouper un ensemble de données en plusieurs variables
  • Utilisation SciPy pour trouver l'optimum d'une fonction

Plongeons dans le monde merveilleux de SciPy!

Différencier SciPy l'écosystème et SciPy la bibliothèque

Lorsque vous souhaitez utiliser Python pour des tâches de calcul scientifique, il existe probablement plusieurs bibliothèques que vous serez invité à utiliser, notamment:

Collectivement, ces bibliothèques constituent le Écosystème SciPy et sont conçus pour fonctionner ensemble. Beaucoup d'entre eux s'appuient directement sur les tableaux NumPy pour effectuer des calculs. Ce didacticiel s'attend à ce que vous ayez une certaine familiarité avec la création de tableaux NumPy et leur fonctionnement.

Dans ce didacticiel, vous découvrirez les Bibliothèque SciPy, l'un des composants essentiels de l'écosystème SciPy. La bibliothèque SciPy est la bibliothèque fondamentale pour le calcul scientifique en Python. Il fournit de nombreuses interfaces efficaces et conviviales pour des tâches telles que l'intégration numérique, l'optimisation, le traitement du signal, l'algèbre linéaire, etc.

Comprendre les modules SciPy

La bibliothèque SciPy est composée de plusieurs modules qui séparent la bibliothèque en unités fonctionnelles distinctes. Si vous souhaitez en savoir plus sur les différents modules inclus dans SciPy, vous pouvez exécuter Aidez-moi() sur scipy, comme indiqué ci-dessous:

>>>

>>> importation scipy
>>> Aidez-moi(scipy)

Cela produit une sortie d'aide pour l'ensemble de la bibliothèque SciPy, dont une partie est illustrée ci-dessous:

Sous-ensembles
-----------

L'utilisation de l'un de ces sous-packages nécessite une importation explicite. Par exemple,
`` import scipy.cluster ''.

::

 cluster --- Quantification vectorielle / Kmeans
 fft --- Transformations de Fourier discrètes
 fftpack --- Transformations de Fourier discrètes héritées
 intégrer --- Routines d'intégration
...

Ce bloc de code montre la Sous-ensembles partie de la sortie d'aide, qui est une liste de tous les modules disponibles dans SciPy que vous pouvez utiliser pour les calculs.

Notez le texte en haut de la section qui dit, "L'utilisation de l'un de ces sous-packages nécessite une importation explicite." Lorsque vous souhaitez utiliser les fonctionnalités d'un module dans SciPy, vous devez importer le module que vous souhaitez utiliser Plus précisément. Vous en verrez quelques exemples un peu plus loin dans le didacticiel, et des instructions pour l'importation de bibliothèques à partir de SciPy sont présentées dans la documentation de SciPy.

Une fois que vous avez décidé quel module vous souhaitez utiliser, vous pouvez consulter la référence de l'API SciPy, qui contient tous les détails sur chaque module dans SciPy. Si vous cherchez quelque chose avec un peu plus d'exposition, alors les notes de cours SciPy sont une excellente ressource pour approfondir de nombreux modules SciPy.

Plus loin dans ce didacticiel, vous découvrirez grappe et optimiser, qui sont deux des modules de la bibliothèque SciPy. Mais d'abord, vous devrez installer SciPy sur votre ordinateur.

Installation de SciPy sur votre ordinateur

Comme avec la plupart des packages Python, il existe deux façons principales d'installer SciPy sur votre ordinateur:

  1. Anaconda
  2. PyPI et pip

Ici, vous apprendrez à utiliser ces deux approches pour installer la bibliothèque. La seule dépendance directe de SciPy est le package NumPy. L'une ou l'autre méthode d'installation installera automatiquement NumPy en plus de SciPy, si nécessaire.

Anaconda

Anaconda est un populaire Distribution de Python, principalement parce qu'il comprend des versions pré-construites des packages Python scientifiques les plus populaires pour Windows, macOS et Linux. Si vous n'avez pas encore installé Python sur votre ordinateur, Anaconda est une excellente option pour commencer. Anaconda est livré préinstallé avec SciPy et ses dépendances requises, donc une fois que vous avez installé Anaconda, vous n'avez rien d'autre à faire!

Vous pouvez télécharger et installer Anaconda à partir de leur page de téléchargements. Assurez-vous de télécharger la version la plus récente de Python 3. Une fois le programme d'installation installé sur votre ordinateur, vous pouvez suivre la procédure de configuration par défaut d'une application, en fonction de votre plate-forme.

Si Anaconda est déjà installé, mais que vous souhaitez installer ou mettre à jour SciPy, vous pouvez également le faire. Ouvrez une application de terminal sur macOS ou Linux, ou l'invite Anaconda sur Windows, et tapez l'une des lignes de code suivantes:

$ conda install scipy
$ mise à jour conda scipy

Vous devez utiliser la première ligne si vous devez installer SciPy ou la deuxième ligne si vous souhaitez simplement mettre à jour SciPy. Pour vous assurer que SciPy est installé, exécutez Python dans votre terminal et essayez d'importer SciPy:

>>>

>>> importation scipy
>>> impression(scipy.__fichier__)
/.../lib/python3.7/site-packages/scipy/__init__.py

Dans ce code, vous avez importé scipy et imprimé l'emplacement du fichier d'où scipy est chargé. L'exemple ci-dessus est pour macOS. Votre ordinateur affichera probablement un emplacement différent. Vous avez maintenant installé SciPy sur votre ordinateur prêt à l'emploi. Vous pouvez passer à la section suivante pour commencer à utiliser SciPy!

Pépin

Si vous avez déjà installé une version de Python qui n'est pas Anaconda, ou si vous ne souhaitez pas utiliser Anaconda, vous utiliserez pépin pour installer SciPy. Pour en savoir plus sur ce pépin est, consultez Qu'est-ce que Pip? Un guide pour les nouveaux pythonistes.

Pour installer SciPy à l'aide de pépin, ouvrez votre application de terminal et saisissez la ligne de code suivante:

$ python -m pip install -U scipy

Le code installera SciPy s'il n'est pas déjà installé ou mettra à niveau SciPy s'il est installé. Pour vous assurer que SciPy est installé, exécutez Python dans votre terminal et essayez d'importer SciPy:

>>>

>>> importation scipy
>>> impression(scipy.__fichier__)
/.../lib/python3.7/site-packages/scipy/__init__.py

Dans ce code, vous avez importé scipy et imprimé l'emplacement du fichier d'où scipy est chargé. L'exemple ci-dessus est pour macOS utilisant pyenv. Votre ordinateur affichera probablement un emplacement différent. Vous avez maintenant installé SciPy sur votre ordinateur. Voyons comment vous pouvez utiliser SciPy pour résoudre quelques problèmes que vous pourriez rencontrer!

Utilisation du module de cluster dans SciPy

Regroupement est une technique populaire pour classer les données en les associant à des groupes. La bibliothèque SciPy comprend une implémentation de la k-means algorithme de clustering ainsi que plusieurs algorithmes de clustering hiérarchiques. Dans cet exemple, vous utiliserez l'algorithme k-means dans scipy.cluster.vq, où vq signifie quantification vectorielle.

Tout d'abord, vous devriez jeter un œil à l'ensemble de données que vous utiliserez pour cet exemple. L'ensemble de données comprend 4827 messages texte réels et 747 (ou SMS) spam. L'ensemble de données brutes peut être trouvé sur le référentiel d'apprentissage automatique UCI ou sur la page Web des auteurs.

Dans l'ensemble de données, chaque message possède l'une des deux étiquettes:

  1. jambon pour les messages légitimes

  2. Spam pour les messages de spam

Le message texte intégral est associé à chaque étiquette. Lorsque vous parcourez les données, vous remarquerez peut-être que les messages de spam contiennent généralement de nombreux chiffres. Ils incluent souvent un numéro de téléphone ou des gains. Prédisons si un message est ou non du spam en fonction du nombre de chiffres dans le message. Pour ce faire, vous devez grappe les données en trois groupes en fonction du nombre de chiffres qui apparaissent dans le message:

  1. Pas de spam: Les messages contenant le plus petit nombre de chiffres ne devraient pas être du spam.

  2. Inconnue: Les messages avec un nombre intermédiaire de chiffres sont inconnus et doivent être traités par des algorithmes plus avancés.

  3. Spam: Les messages avec le plus grand nombre de chiffres devraient être du spam.

Commençons par regrouper les messages texte. Tout d'abord, vous devez importer les bibliothèques que vous utiliserez dans cet exemple:

    1 de pathlib importation Chemin
    2 importation engourdi comme np
    3 de scipy.cluster.vq importation blanchir, kmeans, vq

Vous pouvez voir que vous importez trois fonctions de scipy.cluster.vq. Chacune de ces fonctions accepte un tableau NumPy en entrée. Ces tableaux doivent avoir le Caractéristiques de l'ensemble de données dans les colonnes et la observations dans les rangées.

Une entité est une variable d'intérêt, tandis qu'une observation est créée chaque fois que vous enregistrez chaque entité. Dans cet exemple, il y a 5 574 observations, ou messages individuels, dans l'ensemble de données. De plus, vous verrez qu'il existe deux fonctionnalités:

  1. Le nombre de chiffres dans un SMS
  2. Le nombre de fois ce nombre de chiffres apparaît dans l'ensemble de données

Ensuite, vous devez charger le fichier de données à partir de la base de données UCI. Les données sont fournies sous forme de fichier texte, où la classe du message est séparée du message par un caractère de tabulation et chaque message est sur sa propre ligne. Vous devez lire les données dans une liste en utilisant pathlib.Path:

    4 Les données = Chemin("SMSSpamCollection").read_text()
    5 Les données = Les données.bande()
    6 Les données = Les données.Divisé(" n")

Dans ce code, vous utilisez pathlib.Path.read_text () pour lire le fichier dans une chaîne. Ensuite, vous utilisez .bande() pour supprimer les espaces de fin et diviser la chaîne en une liste avec .Divisé().

Ensuite, vous pouvez commencer en cours d'analyse les données. Vous devez compter le nombre de chiffres qui apparaissent dans chaque message texte. Python comprend collections.Counter dans la bibliothèque standard pour collecter le nombre d'objets dans une structure de type dictionnaire. Cependant, comme toutes les fonctions de scipy.cluster.vq attendez-vous à des tableaux NumPy en entrée, vous ne pouvez pas utiliser collections.Counter pour cet exemple. Au lieu de cela, vous utilisez un tableau NumPy et implémentez les décomptes manuellement.

Encore une fois, vous êtes intéressé par le nombre de chiffres dans un message SMS donné et le nombre de messages SMS qui ont ce nombre de chiffres. Tout d'abord, vous devez créer un tableau NumPy qui associe le nombre de chiffres d'un message donné au résultat du message, qu'il s'agisse de jambon ou de spam:

    sept chiffres_comptes = np.vide((len(Les données), 2), dtype=int)

Dans ce code, vous créez un tableau NumPy vide, chiffres_comptes, qui comporte deux colonnes et 5 574 lignes. Le nombre de lignes est égal au nombre de messages dans l'ensemble de données. Vous utiliserez chiffres_comptes pour associer le nombre de chiffres du message au fait que le message soit ou non du spam.

Vous devez créer le tableau avant d'entrer dans la boucle, afin que vous n'ayez pas à allouer de nouvelle mémoire à mesure que votre tableau se développe. Cela améliore l'efficacité de votre code. Ensuite, vous devez traiter les données pour enregistrer le nombre de chiffres et l'état du message:

    8 pour je, ligne dans énumérer(Les données):
    9     Cas, message = ligne.Divisé(" t")
dix     num_digits = somme(c.isdigit() pour c dans message)
11     chiffres_comptes[[[[je, 0] = 0 si Cas == "jambon" autre 1
12     chiffres_comptes[[[[je, 1] = num_digits

Voici une ventilation ligne par ligne du fonctionnement de ce code:

  • Ligne 8: Boucle sur Les données. Tu utilises énumérer() pour mettre la valeur de la liste dans ligne et créer un index je pour cette liste. En apprendre davantage sur énumérer(), consultez Utilisez enumerate () pour conserver un index en cours d'exécution.

  • Ligne 9: Fractionnez la ligne sur le caractère de tabulation pour créer Cas et message. Cas est une chaîne qui indique si le message est jambon ou Spam, tandis que message est une chaîne avec le texte du message.

  • Ligne 10: Calculez le nombre de chiffres dans le message en utilisant le somme() d'une compréhension. Dans la compréhension, vous vérifiez chaque caractère du message en utilisant isdigit (), qui renvoie Vrai si l'élément est un chiffre et Faux autrement. somme() puis traite chacun Vrai résultat comme 1 et chaque Faux comme un 0. Ainsi, le résultat de somme() sur cette compréhension est le nombre de caractères pour lesquels isdigit () revenu Vrai.

  • Ligne 11: Attribuer des valeurs à chiffres_comptes. Vous affectez la première colonne du je ligne à 0 si le message était légitime (jambon) ou 1 si le message était du spam.

  • Ligne 12: Attribuer des valeurs à chiffres_comptes. Vous affectez la deuxième colonne du je ligne pour être le nombre de chiffres dans le message.

Vous disposez maintenant d'un tableau NumPy qui contient le nombre de chiffres de chaque message. Cependant, vous souhaitez appliquer la algorithme de clustering à un tableau qui a le nombre de messages avec un certain nombre de chiffres. En d'autres termes, vous devez créer un tableau dans lequel la première colonne a le nombre de chiffres dans un message et la deuxième colonne est le nombre de messages qui ont ce nombre de chiffres. Découvrez le code ci-dessous:

13 comptes_uniques = np.unique(chiffres_comptes[:[:[:[: 1], return_counts=Vrai)

np.unique () prend un tableau comme premier argument et renvoie un autre tableau avec les éléments uniques de l'argument. Il prend également plusieurs arguments facultatifs. Ici, vous utilisez return_counts = True instruire np.unique () pour retourner également un tableau avec le nombre de fois que chaque élément unique est présent dans le tableau d'entrée. Ces deux sorties sont renvoyées sous la forme d'un tuple dans lequel vous stockez comptes_uniques.

Ensuite, vous devez transformer comptes_uniques sous une forme adaptée au clustering:

14 comptes_uniques = np.transposer(np.vstack(comptes_uniques))

Vous combinez les deux sorties 1xN de np.unique () dans un tableau 2xN en utilisant np.vstack (), puis les transposer dans un tableau Nx2. Ce format est celui que vous utiliserez dans les fonctions de clustering. Chaque ligne comptes_uniques a maintenant deux éléments:

  1. Le nombre de chiffres dans un message
  2. Le nombre de messages qui avait ce nombre de chiffres

Un sous-ensemble de la sortie de ces deux opérations est illustré ci-dessous:

[[   0 4110]
 [   1  486]
 [   2  160]
 
 
 
 ...
 [  40    4]
 [  41    2]
 [  47    1]]

Dans l'ensemble de données, il y a 4110 messages sans chiffres, 486 avec 1 chiffre, etc. Maintenant, vous devez appliquer l'algorithme de clustering k-means à ce tableau:

15 whitened_counts = blanchir(comptes_uniques)
16 livre de codes, _ = kmeans(whitened_counts, 3)

Tu utilises blanchir() normaliser chaque entité pour avoir une variance unitaire, ce qui améliore les résultats de kmeans (). Alors, kmeans () prend les données blanchies et le nombre de clusters à créer comme arguments. Dans cet exemple, vous souhaitez créer 3 clusters, pour certainement du jambon, certainement spam, et inconnue. kmeans () renvoie deux valeurs:

  1. Un tableau avec trois lignes et deux colonnes représentant les centroïdes de chaque groupe: le kmeans () L'algorithme calcule l'emplacement optimal du centre de gravité de chaque groupe en minimisant la distance entre les observations et chaque centre de gravité. Ce tableau est affecté à livre de codes.

  2. La distance euclidienne moyenne des observations aux centroïdes: Vous n'aurez pas besoin de cette valeur pour le reste de cet exemple, vous pouvez donc l'attribuer à _.

Ensuite, vous devez déterminer à quel cluster appartient chaque observation en utilisant vq ():

17 codes, _ = vq(comptes_uniques, livre de codes)

vq () attribue des codes à partir du livre de codes à chaque observation. Il renvoie deux valeurs:

  1. La première valeur est un tableau de la même longueur que comptes_uniques, où la valeur de chaque élément est un entier représentant le cluster auquel cette observation est affectée. Puisque vous avez utilisé trois clusters dans cet exemple, chaque observation est affectée au cluster 0, 1, ou 2.

  2. La deuxième valeur est un tableau de la distance euclidienne entre chaque observation et son centre de gravité.

Maintenant que les données sont regroupées, vous devez les utiliser pour faire des prédictions sur les messages SMS. Vous pouvez inspecter les décomptes pour déterminer à combien de chiffres l'algorithme de clustering a tracé la ligne entre définitivement ham et inconnu, et entre inconnu et définitivement spam:

18 impression(comptes_uniques[[[[codes == 0][[[[-1])
19 impression(comptes_uniques[[[[codes == 1][[[[-1])
20 impression(comptes_uniques[[[[codes == 2][[[[-1])

Dans ce code, chaque ligne obtient les lignes comptes_uniquesvq () assigné différentes valeurs des codes, soit 0, 1, ou 2. Étant donné que cette opération renvoie un tableau, vous devez obtenir la dernière ligne du tableau pour déterminer le plus grand nombre de chiffres attribués à chaque groupe. La sortie est illustrée ci-dessous:

certainement spam [47  1]
certainement du jambon [   0 4110]
inconnue [20 18]

Dans cette sortie, vous voyez que le certainement du jambon les messages sont les messages avec zéro chiffre dans le message, le inconnue les messages sont tous entre 1 et 20 chiffres, et certainement spam les messages contiennent de 21 à 47 chiffres, ce qui correspond au nombre maximal de chiffres dans votre ensemble de données.

Maintenant, vous devez vérifier la précision de vos prévisions sur cet ensemble de données. Tout d'abord, créez des masques pour chiffres_comptes afin que vous puissiez facilement saisir le jambon ou Spam état des messages:

21 chiffres = chiffres_comptes[:[:[:[: 1]
22 preded_hams = chiffres == 0
23 preded_spams = chiffres > 20
24 preded_unknowns = np.logique_et(chiffres > 0, chiffres <= 20)

Dans ce code, vous créez le preded_hams masque, où il n'y a pas de chiffres dans un message. Ensuite, vous créez le preded_spams masque pour tous les messages de plus de 20 chiffres. Enfin, les messages du milieu sont preded_unknowns.

Ensuite, appliquez ces masques aux nombres de chiffres réels pour récupérer les prédictions:

25 spam_cluster = chiffres_comptes[[[[preded_spams]
26 ham_cluster = chiffres_comptes[[[[preded_hams]
27 unk_cluster = chiffres_comptes[[[[preded_unknowns]

Ici, vous appliquez les masques que vous avez créés dans le dernier bloc de code au chiffres_comptes tableau. Cela crée trois nouveaux tableaux avec uniquement les messages qui ont été regroupés dans chaque groupe. Enfin, vous pouvez voir combien de chaque type de message sont tombés dans chaque cluster:

28 impression("jambons:", np.unique(ham_cluster[:[:[:[: 0], return_counts=Vrai))
29 impression("spams:", np.unique(spam_cluster[:[:[:[: 0], return_counts=Vrai))
30 impression("inconnues:", np.unique(unk_cluster[:[:[:[: 0], return_counts=Vrai))

Ce code imprime le nombre de chaque valeur unique des clusters. N'oubliez pas que 0 signifie qu'un message était jambon et 1 signifie que le message était Spam. Les résultats sont montrés plus bas:

jambons: (tableau ([0, 1]), tableau ([4071,   39]))
spams: (tableau ([0, 1]), tableau ([  1, 232]))
inconnues: (tableau ([0, 1]), tableau ([755, 476]))

De cette sortie, vous pouvez voir que 4110 messages sont tombés dans le certainement du jambon groupe, dont 4071 étaient en fait du jambon et seulement 39 étaient du spam. Inversement, sur les 233 messages tombés dans le certainement spam groupe, seulement 1 était en fait du jambon et le reste était du spam.

Bien sûr, plus de 1 200 messages sont tombés dans le inconnue catégorie, donc une analyse plus avancée serait nécessaire pour classer ces messages. Vous voudrez peut-être examiner quelque chose comme le traitement du langage naturel pour améliorer la précision de votre prédiction, et vous pouvez utiliser Python et Keras pour vous aider.

Utilisation du module Optimiser dans SciPy

Quand vous devez optimiser les paramètres d'entrée d'une fonction, scipy.optimize contient un certain nombre de méthodes utiles pour optimiser différents types de fonctions:

  • minimiser_scalaire () et minimiser() pour minimiser une fonction d'une variable et de plusieurs variables, respectivement
  • curve_fit () pour adapter une fonction à un ensemble de données
  • root_scalar () et racine() pour trouver les zéros d'une fonction d'une variable et de plusieurs variables, respectivement
  • linprog () pour minimiser une fonction objectif linéaire avec des contraintes d'inégalité et d'égalité linéaires

En pratique, toutes ces fonctions remplissent optimisation d'une sorte ou d'une autre. Dans cette section, vous découvrirez les deux fonctions de minimisation, minimiser_scalaire () et minimiser().

Minimiser une fonction avec une variable

Une fonction mathématique qui accepte un nombre et produit une sortie est appelée un fonction scalaire. Il contraste généralement avec les fonctions multivariées qui acceptent plusieurs nombres et entraînent également plusieurs nombres de sortie. Vous verrez un exemple d'optimisation des fonctions multivariées dans la section suivante.

Pour cette section, votre fonction scalaire sera un polynôme quartiqueet votre objectif est de trouver le valeur minimum de la fonction. La fonction est y = 3x⁴ – 2x + 1. La fonction est tracée dans l'image ci-dessous pour une plage de x de 0 à 1:

La fonction y = 3x⁴-2x + 1 tracée sur le domaine de -0,05 à 1,05

Dans la figure, vous pouvez voir qu'il existe une valeur minimale de cette fonction à environ x = 0,55. Vous pouvez utiliser minimiser_scalaire () pour déterminer les coordonnées x et y exactes du minimum. Tout d'abord, importez minimiser_scalaire () de scipy.optimize. Ensuite, vous devez définir la fonction objectif à minimiser:

    1 de scipy.optimize importation minimiser_scalaire
    2 
    3 def objectif_fonction(X):
    4     revenir 3 * X ** 4 - 2 * X + 1

objectif_fonction () prend l'entrée X et lui applique les opérations mathématiques nécessaires, puis renvoie le résultat. Dans la définition de fonction, vous pouvez utiliser toutes les fonctions mathématiques de votre choix. La seule limite est que la fonction doit renvoyer un seul numéro à la fin.

Ensuite, utilisez minimiser_scalaire () pour trouver la valeur minimale de cette fonction. minimiser_scalaire () n'a qu'une seule entrée requise, qui est le nom de la définition de la fonction objectif:

    5 res = minimiser_scalaire(objectif_fonction)

La sortie de minimiser_scalaire () est une instance de Optimiser le résultat. Cette classe rassemble de nombreux détails pertinents de l'exécution de l'optimiseur, notamment si l'optimisation a réussi ou non et, si elle a réussi, quel a été le résultat final. La sortie de minimiser_scalaire () pour cette fonction est indiquée ci-dessous:

                    amusant: 0.17451818777634331
    nfev: 16
     nit: 12
 succès: vrai
       x: 0,5503212087491959

Ces résultats sont tous des attributs de Optimiser le résultat. Succès est une valeur booléenne indiquant si l'optimisation s'est terminée avec succès ou non. Si l'optimisation a réussi, alors amusement est la valeur de la fonction objectif à la valeur optimale X. Vous pouvez voir sur la sortie que, comme prévu, la valeur optimale pour cette fonction était proche de x = 0,55.

Remarque: Comme vous le savez peut-être, toutes les fonctions n'ont pas un minimum. Par exemple, essayez de voir ce qui se passe si votre fonction objectif est y = x³. Pour minimiser_scalaire (), les fonctions objectives sans minimum entraînent souvent une OverflowError car l'optimiseur essaie finalement un nombre trop grand pour être calculé par l'ordinateur.

De l'autre côté des fonctions sans minimum sont fonctions qui ont plusieurs minima. Dans ces cas, minimiser_scalaire () n'est pas garanti pour trouver le minimum global de la fonction. cependant, minimiser_scalaire () a un méthode argument de mot clé que vous pouvez spécifier pour contrôler le solveur utilisé pour l'optimisation. La bibliothèque SciPy possède trois méthodes intégrées pour la minimisation scalaire:

  1. brent est une implémentation de l'algorithme de Brent. Cette méthode est la valeur par défaut.
  2. d'or est une implémentation de la recherche de la section d'or. La documentation note que la méthode de Brent est généralement meilleure.
  3. délimité est une implémentation limitée de l'algorithme de Brent. Il est utile de limiter la zone de recherche lorsque le minimum se situe dans une plage connue.

Quand méthode est soit brent ou d'or, minimiser_scalaire () prend un autre argument appelé support. Il s'agit d'une séquence de deux ou trois éléments qui fournissent une estimation initiale des limites de la région avec le minimum. Cependant, ces solveurs ne garantissent pas que le minimum trouvé sera dans cette plage.

En revanche, lorsque méthode est délimité, minimiser_scalaire () prend un autre argument appelé bornes. Il s'agit d'une séquence de deux éléments qui délimitent strictement la région de recherche pour le minimum. Essayez le délimité avec la fonction y = x⁴ – x². Cette fonction est représentée dans la figure ci-dessous:

La fonction y = x⁴-x² tracée sur le domaine de -1,25 à 1,25

En utilisant l'exemple de code précédent, vous pouvez redéfinir objectif_fonction () ainsi:

    sept def objectif_fonction(X):
    8     revenir X ** 4 - X ** 2

Tout d'abord, essayez la valeur par défaut brent méthode:

    9 res = minimiser_scalaire(objectif_fonction)

Dans ce code, vous n'avez pas transmis de valeur pour méthode, donc minimiser_scalaire () utilisé le brent par défaut. La sortie est la suivante:

                    amusant: -0.24999999999999994
    nfev: 15
     nit: 11
 succès: vrai
       x: 0,7071067853059209

Vous pouvez voir que l'optimisation a réussi. Il a trouvé l'optimum près de x = 0,707 et y = -1/4. Si vous avez résolu analytiquement le minimum de l'équation, vous trouverez alors le minimum à x = 1 / √2, ce qui est extrêmement proche de la réponse trouvée par la fonction de minimisation. Mais si vous vouliez trouver le minimum symétrique à x = -1 / √2? Vous pouvez retourner le même résultat en fournissant le support argument à la brent méthode:

dix res = minimiser_scalaire(objectif_fonction, support=(-1, 0))

Dans ce code, vous fournissez la séquence (-dix) à support pour lancer la recherche dans la région entre -1 et 0. Vous vous attendez à ce qu'il y ait un minimum dans cette région puisque la fonction objectif est symétrique autour de l'axe y. Cependant, même avec support, les brent renvoie toujours le minimum à x = + 1 / √2. Pour trouver le minimum à x = -1 / √2, vous pouvez utiliser le délimité méthode avec bornes:

11 res = minimiser_scalaire(objectif_fonction, méthode='délimité', bornes=(-1, 0))

Dans ce code, vous ajoutez méthode et bornes comme arguments pour minimiser_scalaire ()et vous définissez bornes être compris entre -1 et 0. Le résultat de cette méthode est le suivant:

                    amusant: -0.24999999999998732
 message: 'Solution trouvée.'
    nfev: 10
  statut: 0
 succès: vrai
       x: -0.707106701474177

Comme prévu, le minimum a été trouvé à x = -1 / √2. Notez la sortie supplémentaire de cette méthode, qui comprend un message attribut dans res. Ce champ est souvent utilisé pour une sortie plus détaillée de certains des solveurs de minimisation.

Minimiser une fonction avec de nombreuses variables

scipy.optimize comprend également le plus général minimiser(). Cette fonction peut gérer multivariée entrées et sorties et a des algorithmes d'optimisation plus compliqués pour pouvoir gérer cela. En plus, minimiser() peut gérer contraintes sur la solution à votre problème. Vous pouvez spécifier trois types de contraintes:

  1. LinearConstraint: La solution est contrainte en prenant le produit interne des valeurs x de la solution avec un tableau d'entrée utilisateur et en comparant le résultat à une limite inférieure et supérieure.
  2. NonlinearConstraint: La solution est contrainte en appliquant une fonction fournie par l'utilisateur aux valeurs x de la solution et en comparant la valeur de retour avec une limite inférieure et supérieure.
  3. Bornes: Les valeurs x de la solution sont contraintes de se situer entre une limite inférieure et supérieure.

Lorsque vous utilisez ces contraintes, cela peut limiter le choix spécifique de la méthode d'optimisation que vous pouvez utiliser, car toutes les méthodes disponibles ne prennent pas en charge les contraintes de cette manière.

Essayons une démonstration sur la façon d'utiliser minimiser(). Imaginez que vous êtes un courtier en valeurs mobilières intéressé par maximiser le revenu total de la vente d'un nombre fixe de vos stocks. Vous avez identifié un ensemble particulier d’acheteurs et, pour chaque acheteur, vous connaissez le prix qu’il paiera et le montant d’argent dont il dispose.

Vous pouvez exprimer ce problème comme problème d'optimisation contraint. La fonction objective est que vous souhaitez maximiser vos revenus. cependant, minimiser() trouve la valeur minimale d'une fonction, vous devrez donc multiplier votre fonction objectif par -1 pour trouver les valeurs x qui produisent le plus grand nombre négatif.

Il existe une contrainte sur le problème, qui est que la somme du total des actions achetées par les acheteurs ne dépasse pas le nombre d'actions que vous avez en main. Il y a aussi bornes sur chacune des variables de la solution, car chaque acheteur dispose d'une limite supérieure de trésorerie disponible et d'une limite inférieure de zéro. Les valeurs x de la solution négative signifient que vous paieriez les acheteurs!

Essayez le code ci-dessous pour résoudre ce problème. Tout d'abord, importez les modules dont vous avez besoin, puis définissez des variables pour déterminer le nombre d'acheteurs sur le marché et le nombre d'actions que vous souhaitez vendre:

    1 importation engourdi comme np
    2 de scipy.optimize importation minimiser, LinearConstraint
    3 
    4 n_buyers = dix
    5 n_shares = 15

Dans ce code, vous importez engourdi, minimiser(), et LinearConstraint de scipy.optimize. Ensuite, vous définissez un marché de 10 acheteurs qui vous achèteront 15 actions au total.

Ensuite, créez des tableaux pour stocker le prix que chaque acheteur paie, le montant maximal qu'il peut se permettre de dépenser et le nombre maximal d'actions que chaque acheteur peut se permettre, compte tenu des deux premiers tableaux. Pour cet exemple, vous pouvez utiliser la génération de nombres aléatoires dans np.random pour générer les tableaux:

    6 np.Aléatoire.la graine(dix)
    sept des prix = np.Aléatoire.Aléatoire(n_buyers)
    8 money_available = np.Aléatoire.randint(1, 4, n_buyers)

Dans ce code, vous définissez la valeur de départ pour les générateurs de nombres aléatoires de NumPy. Cette fonction garantit que chaque fois que vous exécutez ce code, vous obtiendrez le même ensemble de nombres aléatoires. C'est là pour vous assurer que votre sortie est la même que le tutoriel pour la comparaison.

À la ligne 7, vous générez la gamme de prix que les acheteurs paieront. np.random.random () crée un tableau de nombres aléatoires sur l'intervalle semi-ouvert[01)Lenombred'élémentsdansletableauestdéterminéparlavaleurdel'instrumentquicontientlenombred'acheteurs[01)Lenombred'élémentsdansletableauestdéterminéparlavaleurdel'instrumentquicontientlenombred'acheteurs[01)Thenumberofelementsinthearrayisdeterminedbythevalueoftheargumentwhichinthiscaseisthenumberofbuyers[01)Thenumberofelementsinthearrayisdeterminedbythevalueoftheargumentwhichinthiscaseisthenumberofbuyers

À la ligne 8, vous générez un tableau d'entiers sur l'intervalle semi-ouvert à partir de[14)ànouveauaveclatailledunombred'acheteursCetableaureprésenteletotaldesachatsdechaqueacheteur[14)ànouveauaveclatailledunombred'acheteursCetableaureprésenteletotaldesachatsdechaqueacheteur[14)againwiththesizeofthenumberofbuyersThisarrayrepresentsthetotalcasheachbuyerhasavailableNowyouneedtocomputethemaximumnumberofshareseachbuyercanpurchase:[14)againwiththesizeofthenumberofbuyersThisarrayrepresentsthetotalcasheachbuyerhasavailableNowyouneedtocomputethemaximumnumberofshareseachbuyercanpurchase:

    9 n_shares_per_buyer = money_available / des prix
dix impression(des prix, money_available, n_shares_per_buyer, SEP=" n")

À la ligne 9, vous prenez le rapport money_available avec des prix pour déterminer le nombre maximum d'actions que chaque acheteur peut acheter. Enfin, vous imprimez chacun de ces tableaux séparés par une nouvelle ligne. La sortie est illustrée ci-dessous:

[0,77132064 0,02075195 0,63364823 0,74880388 0,49850701 0,22479665
 0,19806286 0,76053071 0,16911084 0,08833981]
[1 1 1 3 1 3 3 2 1 1]
[ 1.29647768 48.18824404  1.57816269  4.00638948  2.00598984 13.34539487
 15.14670609  2.62974258  5.91328161 11.3199242 ]

La première ligne est le tableau des prix, qui sont des nombres à virgule flottante compris entre 0 et 1. Cette ligne est suivie par le maximum de trésorerie disponible en nombre entier de 1 à 4. Enfin, vous voyez le nombre d'actions que chaque acheteur peut acheter.

Maintenant, vous devez créer le contraintes et bornes pour le solveur. La contrainte est que la somme du total des actions achetées ne peut pas dépasser le nombre total d’actions disponibles. Il s'agit d'une contrainte plutôt que d'une limite car elle implique plusieurs variables de solution.

Pour représenter cela mathématiquement, on pourrait dire que X[0] + x[1] + ... + x[n] = n_shares, où n est le nombre total d'acheteurs. Plus succinctement, vous pourriez prendre le point ou le produit intérieur d'un vecteur de ceux avec les valeurs de la solution, et contraindre à ce qu'il soit égal à n_shares. N'oubliez pas que LinearConstraint prend le produit scalaire du tableau d'entrée avec les valeurs de la solution et le compare aux bornes inférieure et supérieure. Vous pouvez l'utiliser pour configurer la contrainte sur n_shares:

11 contrainte = LinearConstraint(np.ceux(n_buyers), kg=n_shares, ub=n_shares)

Dans ce code, vous créez un tableau de ceux dont la longueur est n_buyers et passez-le comme premier argument à LinearConstraint. Puisque LinearConstraint prend le produit scalaire du vecteur solution avec cet argument, il en résultera la somme des actions achetées.

Ce résultat est alors contraint de se situer entre les deux autres arguments:

  1. La borne inférieure kg
  2. La limite supérieure ub

Puisque lb = ub = n_shares, c'est un contrainte d'égalité car la somme des valeurs doit être égale à la fois kg et ub. Si kg étaient différents de ub, alors ce serait un contrainte d'inégalité.

Ensuite, créez les limites de la variable de solution. Les limites limitent le nombre d'actions achetées à 0 sur le côté inférieur et n_shares_per_buyer sur le dessus. Le format qui minimiser() attend pour les limites est une séquence de tuples de limites inférieures et supérieures:

12 bornes = [([([([(0, n) pour n dans n_shares_per_buyer]

Dans ce code, vous utilisez une compréhension pour générer une liste de tuples pour chaque acheteur. La dernière étape avant d'exécuter l'optimisation consiste à définir la fonction objectif. Rappelez-vous que vous essayez de maximiser vos revenus. De manière équivalente, vous voulez que le négatif de votre revenu soit aussi grand qu'un nombre négatif possible.

Le revenu que vous générez à chaque vente est le prix que l'acheteur paie multiplié par le nombre d'actions qu'il achète. Mathématiquement, vous pouvez écrire ceci des prix[0]*X[0] + prix[1]*X[1] + ... + prix[n]*X[n], où n est à nouveau le nombre total d'acheteurs.

Encore une fois, vous pouvez représenter cela plus succinctement avec le produit intérieur, ou x.dot (prix). Cela signifie que votre fonction objectif doit prendre les valeurs de solution actuelles X et le tableau des prix comme arguments:

13 def objectif_fonction(X, des prix):
14     revenir -X.point(des prix)

Dans ce code, vous définissez objectif_fonction () pour prendre deux arguments. Ensuite, vous prenez le produit scalaire de X avec des prix et retourner le négatif de cette valeur. N'oubliez pas que vous devez renvoyer le négatif car vous essayez de rendre ce nombre aussi petit que possible ou aussi proche que possible de l'infini négatif. Enfin, vous pouvez appeler minimiser():

15 res = minimiser(
16     objectif_fonction,
17     x0=dix * np.Aléatoire.Aléatoire(n_buyers),
18     args=(des prix,),
19     contraintes=contrainte,
20     bornes=bornes,
21 )

Dans ce code, res est une instance de Optimiser le résultat, comme avec minimize_scalar(). As you’ll see, there are many of the same fields, even though the problem is quite different. In the call to minimize(), you pass five arguments:

  1. objective_function: The first positional argument must be the function that you’re optimizing.

  2. x0: The next argument is an initial guess for the values of the solution. In this case, you’re just providing a random array of values between 0 and 10, with the length of n_buyers. For some algorithms or some problems, choosing an appropriate initial guess may be important. However, for this example, it doesn’t seem too important.

  3. args: The next argument is a tuple of other arguments that are necessary to be passed into the objective function. minimize() will always pass the current value of the solution X into the objective function, so this argument serves as a place to collect any other input necessary. In this example, you need to pass prices à objective_function(), so that goes here.

  4. constraints: The next argument is a sequence of constraints on the problem. You’re passing the constraint you generated earlier on the number of available shares.

  5. bounds: The last argument is the sequence of bounds on the solution variables that you generated earlier.

Once the solver runs, you should inspect res by printing it:

                    fun: -8.783020157087366
     jac: array([-0.77132058, -0.02075195, -0.63364816, -0.74880385,
        -0.4985069, -0.22479665, -0.19806278, -0.76053071, -0.16911077,
        -0.08833981])
 message: 'Optimization terminated successfully.'
    nfev: 204
     nit: 17
    njev: 17
  status: 0
 success: True
       x: array([1.29647768e+00, 3.94665456e-13, 1.57816269e+00, 4.00638948e+00,
       2.00598984e+00, 3.48323773e+00, 5.55111512e-14, 2.62974258e+00,
       5.37143977e-14, 1.34606983e-13])

In this output, you can see message et status indicating the final state of the optimization. For this optimizer, a status of 0 means the optimization terminated successfully, which you can also see in the message. Since the optimization was successful, amusement shows the value of the objective function at the optimized solution values. You’ll make an income of $8.78 from this sale.

You can see the values of X that optimize the function in res.x. In this case, the result is that you should sell about 1.3 shares to the first buyer, zero to the second buyer, 1.6 to the third buyer, 4.0 to the fourth, and so on.

You should also check and make sure that the constraints and bounds that you set are satisfied. You can do this with the following code:

22 impression("The total number of shares is:", sum(res.X))
23 impression("Leftover money for each buyer:" money_available - res.X * prices)

In this code, you print the sum of the shares purchased by each buyer, which should be equal to n_shares. Then, you print the difference between each buyer’s cash on hand and the amount they spent. Each of these values should be positive. The output from these checks is shown below:

The total number of shares is: 15.0
The amount each buyer has leftover is: [4.78506124e-14 1.00000000e+00
 4.95159469e-14 9.99200722e-14 5.06261699e-14 2.21697984e+00 3.00000000e+00
 9.76996262e-14 1.00000000e+00 1.00000000e+00]

As you can see, all of the constraints and bounds on the solution were satisfied. Now you should try changing the problem so that the solver ne peut pas find a solution. Changement n_shares to a value of 1000, so that you’re trying to sell 1000 shares to these same buyers. When you run minimize(), you’ll find that the result is as shown below:

                    fun: nan
     jac: array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])
 message: 'Iteration limit exceeded'
    nfev: 2160
     nit: 101
    njev: 100
  status: 9
 success: False
       x: array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])

Notice that the status attribute now has a value of 9, and the message states that the iteration limit has been exceeded. There’s no way to sell 1000 shares given the amount of money each buyer has and the number of buyers in the market. However, rather than raising an error, minimize() still returns an OptimizeResult instance. You need to make sure to check the status code before proceeding with further calculations.

Conclusion

In this tutorial, you learned about the SciPy ecosystem and how that differs from the SciPy library. You read about some of the modules available in SciPy and learned how to install SciPy using Anaconda or pip. Then, you focused on some examples that use the clustering and optimization functionality in SciPy.

dans le clustering example, you developed an algorithm to sort spam text messages from legitimate messages. En utilisant kmeans(), you found that messages with more than about 20 digits are extremely likely to be spam!

dans le optimization example, you first found the minimum value in a mathematically clear function with only one variable. Then, you solved the more complex problem of maximizing your profit from selling stocks. En utilisant minimize(), you found the optimal number of stocks to sell to a group of buyers and made a profit of $8.79!

SciPy is a huge library, with many more modules to dive into. With the knowledge you have now, you’re well equipped to start exploring!