[PYTHON] NaN Geschichte

Wie immer ist es eine triviale Geschichte, aber sie dient auch als Memo für mich.

Was ist NaN?

Gleitkommazahlen haben eine spezielle Zahl namens NaN (Not a Number), die anomale reelle Werte darstellt. Dies geschieht, wenn Sie unbestimmte Formen wie unendlich-unendlich, 0,0 / 0,0, Quadratwurzeln negativer Zahlen und logarithmische negative Zahlen ausführen, die nicht durch reelle Zahlen dargestellt werden können.

Nachfolgend einige Beispiele.

nan1.cpp


// C++
#include <iostream>
#include <limits>
#include <cmath>

int main() {
  double inf = std::numeric_limits<double>::infinity();
  double a = inf-inf, b = 0.0/0.0, c = sqrt(-1.0), d = log(-1.0);
  std::cout << a << ", " << b << ", " << c << ", " << d << "\n";
  return 0;
}

Nan1.java


// Java
class Nan1 {
  public static void main(String[] args){
    double inf = Double.POSITIVE_INFINITY;
    double a = inf-inf, b = 0.0/0.0, c = Math.sqrt(-1.0), d = Math.log(-1.0);
    System.out.println(a + ", " +  b + ", " + c + ", " + d);
  }
}

nan1.swift


// Swift
import Foundation

let inf = Double.infinity
let a = inf-inf, b = 0.0/0.0, c = sqrt(-1.0), d = log(-1.0)
print(a, ", ", b, ", ", c, ", ", d)

nan1.py


# Python
import numpy as np

inf = np.inf
a = inf-inf
# 0.0/0.0 ist ZeroDivisionError
b = np.sqrt(-1.0)
c = np.log(-1.0)
print(a, ", ", b, ", ", c)

Abhängig von der Sprache kann "nan" "NaN" sein oder es kann subtile Unterschiede geben, aber die Ergebnisse sind im Allgemeinen wie folgt.

nan, nan, nan, nan

In Python verursacht 0.0 / 0.0 ZeroDivisionError.

Übrigens bei ganzen Zahlen

Im Gegensatz zu Gleitkommazahlen haben ** Ganzzahlen keine Möglichkeit, Unendlichkeit oder andere anomale Werte auf die übliche Weise zu speichern. ** Dies bedeutet, dass die Bitdarstellung einer solchen Zahl nicht in der Bitdarstellung einer Ganzzahl zugeordnet ist. Daher ist die Nullteilung von Ganzzahlen in ** C und C ++ undefiniert, und Ausnahmen treten in den meisten anderen Sprachen auf. ** ** **

Betrachten Sie beispielsweise den folgenden C ++ - Code.

zero_division1.cpp


// C++
#include <iostream>

int main() {
  int a = 1/0;
  std::cout << "1/0 = " << a << "\n";
  return 0;
}

Es handelt sich um einen sofort einsatzbereiten Code. Wenn Sie ihn jedoch kompilieren, wird beispielsweise die folgende Warnung angezeigt: Für GCC.

zero_division1.cpp: In function 'int main()':
zero_division1.cpp:5:12: warning: division by zero [-Wdiv-by-zero]
   int a = 1/0;
           ~^~

Für Clang.

zero_division1.cpp:5:12: warning: division by zero is undefined [-Wdivision-by-zero]
  int a = 1/0;
           ^~
1 warning generated.

Wenn Sie im Fall von GCC die Sonderwarnung ohne vorherige Ankündigung ausführen,

$ ./a.out 
Floating point exception: 8

Und Laufzeitausnahmen, Für Clang

$ ./a.out 
1/0 = 215109936
$ ./a.out 
1/0 = 59375920
$ ./a.out 
1/0 = 210141488
$ ./a.out 
1/0 = 234668336
$ ./a.out 
1/0 = 8167728

Und jedes Mal, wenn es ausgeführt wird, wird ein zufälliger Wert ausgegeben. Ich denke, das hängt von der Umgebung ab, aber es ist trotzdem ein undefiniertes Verhalten, also kann ich mich nicht beschweren, egal was passiert.

Andere etwas sicherere (?) Sprachen lösen Ausnahmen aus, z. B. Java:

ZeroDivision.java


// Java
class ZeroDivision {
  public static void main(String[] args) {
    int a = 1/0;
    System.out.println("1/0 = " + a);
  }
}
$ java ZeroDivision
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at ZeroDivision.main(ZeroDivision.java:4)

Eigenschaften von NaN

Kehren wir zur Gleitkommazahl NaN zurück. NaN hat die folgenden Eigenschaften.

a + b
a - b
a * b
a / b

Gibt immer NaN zurück, wenn a oder b NaN ist. Für Java ist es wie folgt.

Nan2.java


// Java
class Nan2 {
  public static void main(String[] args) {
    double a = 1.0, b = Double.NaN;
    System.out.println((a+b) + ", " + (a-b) + ", " + (a*b) + ", " + (a/b));
  }
}
$ java Nan2
NaN, NaN, NaN, NaN

Bei numerischen Berechnungen kommt es daher häufig vor, dass an einem bestimmten Punkt erzeugtes NaN im Handumdrehen übertragen wird und die Ausgabedaten in der Zwischenzeit mit NaN gefüllt werden.

a == b
a > b
a < b
a >= b
a <= b

Gibt immer false zurück, wenn a oder b NaN ist, und auch

a != b

Gibt immer true zurück, wenn a oder b NaN ist. Für Java ist es wie folgt.

Nan3.java


// Java
class Nan3 {
  public static void main(String[] args) {
    double a = 1.0, b = Double.NaN;
    System.out.println((a == b) + ", " + (a > b) + ", " + (a < b) + ", "
      + (a >= b) + ", " + (a <= b) + ", " + (a != b));
  }
}
$ java Nan3
false, false, false, false, false, true

Insbesondere ist ** NaN == NaN falsch! ** ** ** Ein häufiger Fehler bei der Überprüfung, ob x NaN ist, ist also

if (x == Double.NaN) {
  // Do something...
} else {
  // Do something...
}

NaN kann mit dem Code nicht erkannt werden (das else wird immer ausgeführt). Um richtig zu sein, haben die meisten Sprachen ihre eigenen Methoden wie isnan (x) und Double.isNaN (x).

if (Double.isNaN(x)) {
  // Do something...
} else {
  // Do something...
}

Lass uns so schreiben. Nutzen Sie auch die oben genannten Eigenschaften

if (x != x) {
  // Do something...
} else {
  // Do something...
}

Es kann auch NaN nachweisen. Tatsächlich verwendete JavaScript vor ECMAScript 6 eine ähnliche Methode (Informationen zum Bestimmen von NaN. [NaN ist keine Zahl, sondern ein Zahlentyp]. Die Geschichte ist](http://efcl.info/2016/09/06/nan/)). Beachten Sie auch, dass a> b und! (A <= b) nicht äquivalent sind, wenn a oder b NaN sein können.

Beispiel in numerischer Berechnung

Auf dieser Grundlage möchte ich eine Geschichte vorstellen, die ich tatsächlich in der Klasse für numerische Berechnungen an der Universität erlebt habe.

Einer der Algorithmen zum Lösen simultaner Gleichungen ist die Gauß-Seidel-Methode (1 Lösen simultaner Gleichungen (Iterationsmethode). ), [Simultane lineare Gleichungen: Iterative Methode --PukiWiki für PBCG Lab](http://www.slis.tsukuba.ac.jp/~fujisawa.makoto.fu/cgi-bin/wiki/index.php?%CF%A2 % CE% A91% BC% A1% CA% FD% C4% F8% BC% B0% A1% A7% C8% BF% C9% FC% CB% A1))). Werfen wir einen Blick auf die folgenden beiden Implementierungen in C ++ und überlassen die Details dem obigen Link.

gauss_seidel1.cpp


#include <array>
#include <cmath>

template<size_t N>
using vec = std::array<double, N>;
template<size_t N>
using mat = std::array<std::array<double, N>, N>;

template<size_t N>
vec<N> gauss_seidel1(const mat<N>& A, const vec<N>& b) {
  constexpr double epsilon = 1.0e-8;
  vec<N> x1, x2;
  for (size_t i = 0; i < N; i++) {
    x1[i] = x2[i] = 0.0;
  }
  double d;
  do {
    d = 0.0;
    for (size_t i = 0; i < N; i++) {
      double sum = b[i];
      for (size_t j = 0; j < N; j++) {
        if (i != j) {
          sum -= A[i][j]*x2[j];
        }
      }
      x2[i] = sum/A[i][i];
      d += abs(x1[i]-x2[i]);
    }
    x1 = x2;
  } while (d > epsilon);
  return x2;
}

gauss_seidel2.cpp


#include <array>
#include <cmath>

template<size_t N>
using vec = std::array<double, N>;
template<size_t N>
using mat = std::array<std::array<double, N>, N>;

template<size_t N>
vec<N> gauss_seidel2(const mat<N>& A, const vec<N>& b) {
  constexpr double epsilon = 1.0e-8;
  vec<N> x1, x2;
  for (size_t i = 0; i < N; i++) {
    x1[i] = x2[i] = 0.0;
  }
  while (true) {
    double d = 0.0;
    for (size_t i = 0; i < N; i++) {
      double sum = b[i];
      for (size_t j = 0; j < N; j++) {
        if (i != j) {
          sum -= A[i][j]*x2[j];
        }
      }
      x2[i] = sum/A[i][i];
      d += abs(x1[i]-x2[i]);
    }
    if (d <= epsilon) break;
    x1 = x2;
  }
  return x2;
}

Sie müssen sich nicht um die Details kümmern. Was ich möchte, dass Sie sehen, ist die Behandlung von d. Dieses d stellt den "Abstand (um genau zu sein die L1-Norm)" dar, wenn der Vektor des vorherigen Schritts x1 und der Vektor des aktuellen Schritts x2 ist. Und die erstere Implementierung iteriert weiter, während dieses d größer als epsilon ist, und die letztere besteht darin, die Iteration zu stoppen, wenn d kleiner oder gleich epsilon ist, und was macht man schließlich? Sieht fast gleich aus. In der Tat beide Implementierungen

\left(
  \begin{array}{ccc}
    3 & 1 & 1 \\
    1 & 3 & 1 \\
    1 & 1 & 3
  \end{array}
\right)
\bf{x}
=
\left(
  \begin{array}{c}
    0 \\
    4 \\
    6
  \end{array}
\right)

Lösung von

\bf{x} = 
\left(
  \begin{array}{c}
    -1 \\
     1 \\
     2
  \end{array}
\right)

Ist richtig angegeben. Diese Gauß-Seidel-Methode hat jedoch ihre Nachteile. das ist,

\left(
  \begin{array}{ccc}
    1 & 2 & 2 \\
    2 & 1 & 2 \\
    2 & 2 & 1
  \end{array}
\right)
\bf{x}
=
\left(
  \begin{array}{c}
     1 \\
     0 \\
    -1
  \end{array}
\right) \\
\bf{x} = 
\left(
  \begin{array}{c}
    -1 \\
     0 \\
     1
  \end{array}
\right)

Selbst wenn es sich um eine Simultangleichung handelt, die eine eindeutige Lösung aufweist, kann die Lösung nur erhalten werden, wenn die Koeffizientenmatrix diagonal dominant ist. Insbesondere divergieren die x1 [i] - und x2 [i] -Werte im obigen Code und werden schließlich zu NaN. Da dann x1 und x2 verwendet werden, um x1 und x2 zu finden, bleibt NaN, sobald es aus der obigen Eigenschaft 1 erzeugt wurde, danach NaN. Daher ist d, das aus x1 und x2 erhalten wird, immer NaN. Daher bleiben ab Eigenschaft 2 sowohl d> epsilon als auch d <= epsilon falsch.

......

Hast du es schon bemerkt? Wenn Sie eine Lösung nicht gut finden und sie divergiert, können Sie in Implementierung 1 aus der Schleife herauskommen, in Implementierung 2 jedoch in einer Endlosschleife. Selbst wenn der Algorithmus korrekt ist, funktioniert er je nach eingegebenem Wert möglicherweise nicht. Wenn also das Risiko besteht, müssen Sie geeignete Maßnahmen ergreifen (im obigen Fall die maximale Anzahl von Iterationen festlegen). , Eine Ausnahme auslösen, wenn es nicht funktioniert).

Recommended Posts

NaN Geschichte
Die Geschichte von Python und die Geschichte von NaN