Tous droits réservés

Quand la calculatrice nous trompe

Calcul numérique et perte de précision

« Vous n'aurez pas toujours une calculatrice dans la poche ! » Cette phrase, prononcée par tant de prof de maths, tombe en désuétude à l'heure où les téléphones glissés dans nos poches sont parfois plus puissants que les supercalculateurs d'il y a 30 ans. Mais à quel point pouvons-nous faire confiance aux machines à calculer pour fournir les bons résultats ?

Les ordinateurs actuels, qui sont tout simplement de grosses machines à calculer, enregistrent généralement les nombres sur 64 bits, ce qui permet de représenter une quinzaine de chiffres décimaux. On aimerait qu'après quelques calculs avec ces nombres, le résultat bénéficie également d'une telle précision. C'est souvent le cas, mais pas toujours.

En effet, des calculs simples peuvent donner des résultats faux dès les premiers chiffres, malgré la quinzaine de chiffres stockés par la machine ! Ces cas sont relativement rares, mais lorsqu'ils concernent la trajectoire d'une fusée ou la résistance d'un gratte-ciel, cela peut porter à conséquence. Surtout que ce type d'erreurs est dur à détecter, le résultat du calcul étant, a priori, inconnu.

Cet article montre comment il est simple de se laisser tromper par un programme de calcul en apparence juste. Il prend pour exemple un programme de résolution des équations du second degré, qui présente (au moins) une erreur discrète affectant sensiblement les résultats. Sa correction n'est pas triviale, et montre que les calculs faits par ordinateur sans précaution ne sont pas aussi infaillibles que l'on pourrait le penser au premier abord.

Si vous êtes familier avec les équations du second degré et que vous avez déjà touché une calculatrice dans votre vie, vous êtes prêts pour une virée au pays parfois surprenant du calcul numérique. C'est parti !

Programme simple

Par définition, une équation du second degré peut toujours s'écrire de la forme suivante :

$$a x^2 + bx + c = 0$$

avec $a$, $b$, $c$ trois nombres réels et $a$ non-nul.

Pour la résoudre, rien de plus simple que de partir de la méthode classique, telle qu'enseignée au lycée. Elle consiste à :

  1. Calculer le discriminant : $\Delta = b^2 - 4ac \,.$
  2. Calculer les solutions en fonction de la valeur du discriminant $\Delta$.
    • S'il est négatif, il n'y a pas de solution réelle.
    • S'il est nul, l'unique solution est $ x_1 = -\frac{b}{2a}$.
    • S'il est positif, les deux solutions sont $ x_1 = \frac{-b+\sqrt{\Delta}}{2a}$ et $x_2 = \frac{-b-\sqrt{\Delta}}{2a}$.

Voici un programme de résolution des équations du second degré utilisant cette méthode, écrit en Python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from math import sqrt

# Définition des coefficients
a = 1.0  # doit être non-nul !
b = -4.0
c = 3.0

# Résolution de l'équation
delta = b * b - 4 * a * c
if delta < 0:  # pas de solution réelle
    solutions = []
elif delta == 0:  # une seule solution
    solutions = [-b / (2 * a)]
elif delta > 0:  # deux solutions
    solutions = [(-b + sqrt(delta)) / (2 * a), (-b - sqrt(delta)) / (2 * a)]
else:  # impossible
    print("Euh... Kamoulox ?")

# Affichage du résultat

print("Δ = {}".format(delta))
for i, x in enumerate(solutions):
    print("x_{} = {}".format(i + 1, x))

En testant rapidement le programme, on constate qu'il se comporte bien au premier abord. Par exemple pour $a = 1$, $b = -4$ et $c = 3$, il affiche le résultat correct suivant.

1
2
3
Δ = 4.0
x_1 = 3.0
x_2 = 1.0

Identification de l'erreur

Cas problématique

Notre petit programme semble fonctionner, mais fonctionne-t-il quelles que soient les valeurs des coefficients $a$, $b$ et $c$ ? Regardons en détail le cas $ a = 1 $, $ b = -10^{7} $ et $ c = 1 $.

Pour ces valeurs, notre programme renvoie les résultats ci-dessous.

1
2
3
Δ = 99999999999996.0
x_1 = 9999999.9999999
x_2 = 9.96515154838562e-08

Cependant, les approximations des solutions exactes1, sont les suivantes :

$$ x \approx 9.999999999999... \times 10^6 $$

$$x \approx 1.000000000000... \times 10^{-7} $$

On voit que si la première solution semble correcte, la deuxième présente une erreur dès le troisième chiffre. Notre programme fournit donc un résultat faux, alors même qu'un ordinateur peut normalement calculer avec une quinzaine de chiffres. D'où vient l'erreur ?

Source du problème

Pour tenter de comprendre où se glisse l'erreur, calculons la solution étape par étape. La première étape consiste à calculer le discriminant.

1
2
>>> b*b -4*a*c
99999999999996.0

Il s'agit de la valeur exacte, il n'y a pas de problème. Continuons en calculant la racine carrée.

1
2
>>> math.sqrt(b*b -4*a*c)
9999999.9999998

Une approximation de la racine carrée exacte donne un résultat similaire, il n'y a pas non plus de problème. Continuons encore.

1
2
>>> -b - math.sqrt(b*b -4*a*c)
1.993030309677124e-07

Ce n'est pas totalement à coté de la plaque, mais le résultat exact de ce calcul s'arrondit comme suit :

$$ 2.00000000000... \times 10^{-7} $$

Le quatrième chiffre de notre calcul est donc faux. Il y a anguille sous roche !

Cause du problème

Pourquoi n'a-t-on que trois chiffres justes alors qu'on pourrait en espérer au moins une dizaine ?

Le problème vient du fait que $\sqrt{b^2 - 4ac}$ est très proche de $-b$, ce qui est désavantageux lors de la soustraction.

Regardons schématiquement ce qu'il se passerait si on stockait exactement 16 chiffres décimaux.

$$ -b - \sqrt{b^2 - 4ac} = 10\,000\,000{,}\,000\,000\,00 - 9\,999\,999{,}\,999\,999\,801 = 0{,}\,000\,000\,199 = 1,99 \times 10^{-7}$$

Lors du calcul, de nombreux chiffres s'annulent, et seuls les derniers chiffres apparaissent effectivement dans le résultat final… Cela signifie que les chiffres nécessaires pour un résultat précis sont gâchés par des chiffres sans importance, car commun aux deux termes de la soustraction. Dans un tel cas, au lieu d'exploiter une quinzaine de chiffres, le résultat n'en exploite qu'une poignée (trois dans notre exemple), et est donc très imprécis.

Ce phénomène apparaissant dès que l'on soustrait deux nombres proches s'appelle une annulation catastrophique ou annulation massive, et nuit gravement à la précision des calculs. Peut-on y remédier dans notre cas ?


  1. Obtenues avec Wolfram Alpha, qui utilise des méthodes spécifiques pour calculer avec autant de décimales que souhaitées. On parle de calcul en précision arbitraire. 

Résolution de l'erreur

$\DeclareMathOperator{\signe}{signe}$

Pour résoudre le problème d'annulation catastrophique, il existe plusieurs voies, parmi lesquelles :

  • utiliser un système de calcul formel pour obtenir une solution exacte,
  • calculer avec plus de décimales pour mitiger l'annulation catastrophique,
  • utiliser des fractions, avec lesquelles on peut calculer de manière exacte,
  • contourner l'annulation catastrophique pour éviter de soustraire deux nombres proches.

Les deux premières solutions sont simples, et devraient être utilisées si possible, en particulier par les non-spécialistes. Elles ont cependant le désavantage d'être peu performantes, et donc inadaptées au calcul intensif, mais également de ne pas être utilisables sur de nombreux dispositifs tels que les calculatrices et les calculateurs embarqués1.

La solution utilisant les fractions n'est pas toujours adaptée, ni performante d'ailleurs, en particulier quand il s'agit d'effectuer des calculs dont le résultat peut être irrationnel (racine carrée typiquement).

Nous allons donc voir une solution utilisant la quatrième voie, c'est-à-dire qui contourne la soustraction problématique.

En regardant notre problème de près, on remarque que si $\sqrt{b^2 - 4ac}$ est proche de $b$, deux cas apparaissent :

  • soit $b$ est positif et il faut éviter de calculer $-b + \sqrt{b^2 - 4ac}$,
  • soit $b$ est négatif et il faut éviter de calculer $-b - \sqrt{b^2 - 4ac}$.

Autrement dit, il faut éviter de calculer la solution suivante directement :

$$ x_1 = \frac{-b + \signe(b) \sqrt{b^2 - 4ac}}{2a} ~,$$

avec $\signe(b)$ égal à 1 pour b positif ou nul et -1 sinon.

Par contre, rien ne nous empêche de calculer l'autre solution comme précédemment.

$$ x_2 = \frac{-b - \signe(b) \sqrt{b^2 - 4ac}}{2a} ~.$$

Pour calculer $x_1$ sans utiliser la formule habituelle, on peut utiliser la propriété suivante qui lie les deux solutions de l'équation :

$$ x_1 x_2 = \frac{c}{a} $$

On en déduit la formule suivante, qui n'a plus d'annulation catastrophique.

$$ x_1 = \frac{c}{ax_2} = \frac{2c}{-b-\signe(b)\sqrt{b^2-4ac}}$$

En mettant à jour le programme initial avec notre astuce, on obtient la version améliorée ci-dessous.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from math import sqrt

# Définition des coefficients
a = 1
b = -1e7
c = 1

# Résolution de l'équation
delta = b * b - 4 * a * c
if delta < 0:  # pas de solution réelle
    solutions = []
elif delta == 0:  # une seule solution
    solutions = [-b / (2 * a)]
elif delta > 0:  # deux solutions
    signe_b = 1 if b >= 0 else -1
    solutions = [(2 * c) / (-b - signe_b * sqrt(delta)), (-b - signe_b * sqrt(delta)) / (2 * a)]
else:  # impossible
    print("Euh... Kamoulox ?")

# Affichage du résultat
print("Δ = {}".format(delta))
for i, x in enumerate(solutions):
    print("x_{} = {}".format(i, x))

On teste avec les valeurs problématiques de tout à l'heure et on obtient :

1
2
3
Δ = 99999999999996.0
x_1 = 1.00000000000001e-07
x_2 = 9999999.9999999

Ce qui est très proche des approximations des valeurs exactes :

$$x \approx 1.000000000000... \times 10^{-7} $$

$$ x \approx 9.999999999999... \times 10^6 $$

Problème résolu !


  1. Par exemple, les très populaires cartes Arduino ne sont pas adaptées au calcul formel et sont limitées à 32 bits pour la représentation des nombres. 


L'exemple de cet article, la résolution d'équations du second degré, peut sembler un problème sans grand intérêt, tant il est simple. Pourtant, nous avons vu comment un calcul numérique peu précautionneux peut fournir des résultats surprenants, alors même que le programme de calcul semble en apparence juste.

Le phénomène d'annulation massive nous a causé des problèmes, et nous avons pu l'éviter sans sortir l'artillerie lourde (calcul formel, calcul avec nombre de chiffres augmenté). L'origine de l'annulation massive se trouve dans le nombre restreint de chiffres utilisés pour stocker les nombres. Cette particularité du calcul par ordinateur est la source de nombreux autres problèmes, étudiés par les chercheurs et ingénieurs en analyse numérique.

Alors, peut-on faire confiance à sa calculatrice ? Oui, mais si on veut un calcul précis, alors il faut tenir compte des limitations des ordinateurs et être éventuellement astucieux. Il n'existe pas de solution miracle aux problèmes de précision. Si certains conseils pratiques peuvent aider (éviter les soustractions quand les deux termes peuvent être proches, par exemple), ils ne sont pas forcément suffisants pour analyser un problème spécifique. Le domaine reste pointu, et les logiciels spécialisés exploitent de nombreuses techniques pour offrir des outils de calcul à la fois précis et performants.

Références pour aller plus loin


Merci à SpaceFox, ρττ, Karnaj et Gabbro pour leurs retours lors de la bêta, ainsi qu'à artragis et Arius pour la validation.

Illustration du tutoriel : Photographie d'une calculette par Adrian Pingstone, 2004, domaine public.

Ces contenus pourraient vous intéresser

18 commentaires

Merci beaucoup pour ce bel article, Aabu.

C'est vraiment quelque chose auquel il faut faire attention. Ce genre d'erreur arrive plus vite qu'on ne le pense, et est très dur à débusquer…

+0 -0

Nous allons donc voir une solution utilisant la troisième voie, c'est-à-dire qui contourne la soustraction problématique.

Résolution de l'erreur

Tu utilises pourtant la quatrième solution listé. J'imagine que tu en as rajouté une en cours de route.

tleb

C'était une erreur d'arrondi, c'est corrigé.

+6 -0

Je pense que ta blague date du temps où les ordi étaient en 32 bits :

1
2
>>> 999999999999999-999999999999997
2

Et pour ceux qui se demandent si ce genre d'erreur est facilement prédictible, je vous laisse seuls juges :

1
2
3
4
5
6
7
8
>>> 999999999999999 -999999999999997
2
>>> 999999999999999.-999999999999997.
2.0
>>> 999999999999999999999999999999 -999999999999999999999999999997
2
>>> 999999999999999999999999999999.-999999999999999999999999999997.
0.0

:-°

+5 -0

Le problème est extrêmement présent quand on fait un peu de calcul numérique, l'exemple typique est le conditionnement des matrices. Quand on cherche à résoudre $Ax=b$ avec une matrice $A$ mal conditionnée, un petit changement dans $b$ peut entrainer de gros changements dans $x$. Quand on enchaine les calculs, de petites imprécisions qui se propagent peuvent être terribles.

Le problème est extrêmement présent quand on fait un peu de calcul numérique, l'exemple typique est le conditionnement des matrices. Quand on cherche à résoudre $Ax=b$ avec une matrice $A$ mal conditionnée, un petit changement dans $b$ peut entrainer de gros changements dans $x$. Quand on enchaine les calculs, de petites imprécisions qui se propagent peuvent être terribles.

Davidbrcz

Le calcul formel c'est trop cool =) !

Le problème est extrêmement présent quand on fait un peu de calcul numérique, l'exemple typique est le conditionnement des matrices. Quand on cherche à résoudre $Ax=b$ avec une matrice $A$ mal conditionnée, un petit changement dans $b$ peut entrainer de gros changements dans $x$. Quand on enchaine les calculs, de petites imprécisions qui se propagent peuvent être terribles.

Davidbrcz

Le calcul formel c'est trop cool =) !

Saroupille

Non.

C'est un outil et comme tout outil il peut ou non être pertinent.

Quand on fait des calculs de mode propre par exemple, à la fin on veut des valeurs numériques en Hertz avec une certaine précision. On se fiche que la valeur soit $\sqrt{2-\frac{\pi}{4}}$. Ce qu'on souhaite, c'est la stabilité numérique =).

Ensuite, je ne suis pas sûr que les systèmes formels puissent encaisser des matrices de 100 000 par 100 000.

+2 -0

Il faut y faire très attention, mais j’ai toujours trouvé ça très beau au final. Au premier abord ça semble magique, alors que ce n’est simplement que technologique et très facile à comprendre. Notamment grâce à ce genre d’article bien construit avec des exemples concrets !

Très bon article !

Par contre, pourrons nous un jour régler ce problème définitivement ?

Par contre, pourrons nous un jour régler ce problème définitivement ?

Jugid

Il existe des techniques d’optimisation qui, prenant en compte les bornes sur les valeurs de travail, sont capables de minimiser l’écart à la valeur exacte d’un calcul.

Cependant, ce n’est pas une technique qui peut être utilisée dans l’absolu car selon où l’on se place dans les ranges flottant, l’écart entre eux changent et donc la manière de faire le calcul de manière la plus exacte possible aussi.

Après il reste la solution de changer le système d’approximation, mais ça implique aussi de changer nos processeurs.

Bonjour,

Quand on n’est pas développeur on passe facilement à coté de ce genre de problèmes et on a pas forcément les compétence pour les résoudre,mais est ce que l’utilisation de nombres en précision arbitraire, comme le type decimal en python, permettrait d’éviter ces erreurs ?

Edit: je suis conscient que cela implique souvent des pertes de performances, mais parfois je préfére avoir ne réponse juste plutôt que rapide.

+0 -0

Quand on n’est pas développeur on passe facilement à coté de ce genre de problèmes et on a pas forcément les compétence pour les résoudre,mais est ce que l’utilisation de nombres en précision arbitraire, comme le type decimal en python, permettrait d’éviter ces erreurs ?

Oui, mais est ce que tu as besoin de ce niveau de précision ? Au delà d’un certain nombre de décimales, les différences deviennent peu intéressantes pour être considérées. La difficulté réelle avec les valeurs float, c’est :

  • que la valeur d’erreur est variable selon les valeurs que l’on manipule,
  • que la propagation d’une erreur qui peut rendre le résultat vraiment complètement faux,
  • que les propriétés habituelles des opérations (réflexivité, associativité, etc) ne sont plus vraies.

Dans ce genre de cas finalement, on peut se demander si ce qu’on a envie c’est pas simplement de travailler avec de la virgule fixe (plutôt qu’une virgule flottante, qui nous donne une précision variable), sur laquelle :

  • il est très facile de prévoir ce que l’on perd : c’est précisément ce qui est hors bornes de la partie entière ou de la partie décimale. Et ça pour le coup, on s’en soucie généralement peu quand on travaille sur les entiers, on a l’habitude, on sait faire,
  • la propagation de l’erreur est en conséquence, aisément mesurable d’une opération à l’autre,
  • les propriétés dont on a l’habitude sont encore nombreuses à être vraies (mais pas forcément parfaitement).

C’est pour ça que pour le monétaire, c’est ce qu’on utilise, parce qu’en dessous du centime généralement, on se fout de la différence, et des flottant ne nous apporteraient que des ennuis, parce que même 0.1 euros, ça fait pas 10 centimes en flottant ;) .

des flottant ne nous apporteraient que des ennuis, parce que même 0.1 euros, ça fait pas 10 centimes en flottant ;) .

Ksass`Peuk

C’est tout à fait vrai avec les flottants usuels, à cause des erreurs d’arrondi entre le système décimal usuel et le système binaire des flottants. Il existe cependant des formats flottants décimaux, qui servent justement à éviter les arrondis liés au système de numération. Ils conservent cependant tous les autres désagréments (ou avantages selon le point de vue). :D

@kayou : En général, tu n’as vraiment pas à t’inquiéter. En tant que développeur, une culture générale sur le sujet suffit (savoir par exemple que tester une égalité entre flottants est en général une mauvaise idée).

Les flottants ont été inventés comme nombres à virgule généralistes. Ils font l’affaire dans la plupart des cas : physique, ingénierie, mathématiques élémentaires, finance personnelle, jeux vidéo, etc. C’est vraiment quand on commence à avoir des besoins spéciaux que ça peut devenir gênant : codes de simulation précis ou critiques (aéronautique par exemple), systèmes bancaires (imagine perdre de l’argent par erreur d’arrondi !), mathématiques (par exemple si on utilise des calculs numériques pour aider à prouver des bornes dans des théorèmes).

Si tu veux en savoir plus, je te conseille de lire les deux tutos suggérés à la fin de l’article dans « Ces contenus pourraient vous intéresser ».

+1 -0

Merci @Abbu

j’ai repris ton script python mais en utilisant Decimal, et ca ne marche pas, TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float' parce que l’utilisation de math.sqrt renvoie toujours un float.

Nota, je ne suis pas développeur , depuis quelques temps j’utilise python pour remplacer Excel (qui ne s’en sort pas mieux sur cet exemple), et ceci me rapelle un cas où je voulais calculer des défauts de positionnements entre 2 pieces ayant un contact sphériques, il y une dizaines de paramètres mesurables allant du nanomètre au centimetre qui entrent en jeux, c’est de la géométrie pure établie en suivant des chaines de cotes (donc avec des soustractions potentiellment catastrophiques ;) ), les résultats obtenus ne correspondant pas à la réalité on s’est rabattu sur des données statistiques réelles pour être ne mesure de faire des estimations du défaut.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte