0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

doubleとDoubleのまとめ

0
Posted at

浮動小数点型って、今までほとんど使ったことなかったので、AIチャットでの回答のまとめ。

きっかけは、x * Math.log(x)を計算する際に、x=0において、Math.log(0)=-Infinityとなりx*log(x)NaNとなってしまうのを避ける方法。なお、log(x)は事前計算してキャッシュするので、x=0にて条件分岐できない(何か有効な値が必要)。と思ってた。

おそらくMath.log(Math.max(x, 1e-300))がよさげ(ただしx>=0であること)。

普通に聞いたら、1e-15くらいでいいんじゃねと回答が来たけど、x=1e-50が普通にあったら逆転するんじゃ?と突っ込むとそりゃダメだ、もっと小さく1e-100と回答が来た。
汎用的にlog関数を用意するのなら、1e-300にしておけば、これより小さいxとの誤差とか考えなくてよいかな。

ところで、
ln(1e-300)≒-690.775なのか。ln(0)=-∞とはえらい違いだ。
もしx==0で判定して何か定数を入れるなら、-999を入れるみたいな感じか。(x==0) ? -999 : Math.log(x)
なんか昭和っぽい雰囲気だけど、そっちの方がlog計算時間を省けるので、100万件の配列にほとんど0の場合に-999を入れる方が速いわな。

反対にexp(y)で戻すときにexp(-999)を計算したら0.0exp(log(1e-300))を計算したら1.0000000000000237E-300。もともとx=0なので、-999でもいいのか。


double型の有効範囲

Javaのdouble型(64ビット浮動小数点数)は、IEEE 754規格に基づいた非常に広い範囲をカバーしていますが、その「仕組み」を理解しておくと計算ミスを防ぎやすくなります。


1. 数値としての有効範囲

double型が表現できる最小値と最大値は、Doubleクラスの定数で定義されています。

項目 定数名 およその値
正の最大値 Double.MAX_VALUE $1.79 \times 10^{308}$
正の最小値(正の 0 に最も近い) Double.MIN_VALUE $4.9 \times 10^{-324}$
負の最小値(絶対値が最大) -Double.MAX_VALUE $-1.79 \times 10^{308}$

2. 「精度」の限界(有効桁数)

範囲は広いですが、**「正確に表現できる桁数」**には限界があります。

  • 有効桁数: 約15〜17桁(10進数換算)
  • この桁数を超えると、どんなに大きな数値や小さな数値でも「正確な1の位」などは保持できず、近似値になります。

3. 特殊な値

double型には、通常の数値以外に3つの特殊な状態が存在します。

  1. Positive Infinity (正の無限大):
    Double.MAX_VALUE を超える計算結果になった場合。
  2. Negative Infinity (負の無限大):
    先ほどの Math.log(0) の結果など。
  3. NaN (Not a Number / 非数):
    0.0 / 0.0Math.sqrt(-1.0) など、数学的に定義できない計算の結果。

4. 範囲外になった時の挙動(オーバーフロー・アンダーフロー)

  • オーバーフロー: 最大値を超えると自動的に Infinity になります。エラー(例外)は投げられません。
  • アンダーフロー: あまりに 0 に近い小さな値(Double.MIN_VALUE 未満)になると、静かに 0.0 として扱われます。

javaのMath.logで-INFを防ぐ方法

JavaのMath.log(x)で戻り値が -Infinity になるのは、引数 x0.0(または非常に 0 に近い値)の場合です。

対数関数 $y = \log_e(x)$ は $x$ が 0 に近づくほど $-\infty$ に向かう性質があるため、これを防ぐには「0 を入力しない」ための工夫が必要です。


1. 微小値を加算する(イプシロン法)

実務で最も一般的な方法です。$x$ に非常に小さな値(マシンイプシロンなど)を足すことで、$x$ が 0 の場合でも計算を破綻させないようにします。

double epsilon = 1e-10; // 用途に合わせて調整(1e-7 ~ 1e-15など)
double result = Math.log(value + epsilon);

2. Math.max で下限を制限する

値が一定の閾値を下回らないようにクリッピング(制限)する方法です。コードの意図が明確になります。

double minValue = 1e-15;
double result = Math.log(Math.max(value, minValue));

3. Math.log1p を使用する

もし計算したい内容が $\log(1 + x)$ の形式であれば、Java標準の Math.log1p(double x) を使うのがベストです。
これは $x$ が 0 に非常に近い場合でも、精度を落とさずに計算できるように設計されています。


NaNって出てきたので計算式を追ったら、0*log(0)でした。

それは厄介なパターンに当たりましたね。数学的には $x \log(x)$ の $x \to 0$ における極限は 0 に収束しますが、コンピュータ(JavaやC++)の計算機自然数ではそうはいきません。

原因は、計算の優先順位にあります。

なぜ NaN になるのか?

  1. まず Math.log(0) が計算され、 -Infinity になります。
  2. 次に 0 * -Infinity が計算されます。
  3. 浮動小数点のルール(IEEE 754)では、「ゼロ掛ける無限大」は定義不能なため、結果は NaN となります。

解決策:どう書くのが正解?

この計算(特にエントロピー計算や機械学習の損失関数などでよく出ます)を安全に行うには、いくつかの回避策があります。

1. if文で 0 を弾く(最も確実)

$x=0$ のときは $0 \log(0) = 0$ とみなして良い文脈であれば、単純に条件分岐させるのが最も安全です。

double x = 0.0;
double result = (x == 0) ? 0 : x * Math.log(x);

2. 微小値を加算する(前述のイプシロン法)

微小な値 $\epsilon$ を足すことで、ログの中身が 0 になるのを防ぎます。

double epsilon = 1e-15;
double result = x * Math.log(x + epsilon);

※ただし、これでも $x$ 自体が 0 なら結果は 0 になり、NaN は回避できます。

3. 閾値でクリップする

Math.max を使って、計算対象が極端に小さな値にならないように制限します。

double result = x * Math.log(Math.max(x, 1e-15));


数学的な補足:ロピタルの定理

数学の授業で習ったかもしれませんが、関数 $f(x) = x \ln x$ は、ロピタルの定理を用いると以下のように証明できます。

$$\lim_{x \to 0} x \ln x = \lim_{x \to 0} \frac{\ln x}{1/x} = \lim_{x \to 0} \frac{1/x}{-1/x^2} = \lim_{x \to 0} (-x) = 0$$

コンピュータはこの「極限」を自動で判断してくれないため、「0 のときは 0 になるはずだ」という数学的知識をコードで明示的に教えてあげる必要があります。


数1とNaNを大小比較すると大きいのはどちら

結論から言うと、NaN(非数)と数値を比較した場合、結果は常に false になります。

つまり、「どちらが大きいか」という問いに対しては、**「どちらも大きくない(比較不能)」**というのがコンピュータ上の回答です。


1. JavaやC++での比較結果

具体的にコードで比較すると、以下のようになります。

double a = 1.0;
double b = Double.NaN;

System.out.println(a > b);  // false
System.out.println(a < b);  // false
System.out.println(a == b); // false

驚くべきことに、NaN == NaN ですら false になります。これは IEEE 754 規格で「NaNは何物とも等しくない」と定義されているためです。

2. なぜこうなるのか?

NaN は「数値ではない何か(計算不能な状態)」を指します。
例えば、「身長 170cm の人と、数値化できない幽霊のどちらの背が高いか?」と聞かれても、比較の土俵に上がっていないため「幽霊の方が高い」とも「170cmの方が高い」とも言えないのと同じ理屈です。


3. 注意が必要なケース(バグの温床)

この性質を知らないと、プログラムに**「すり抜け」**が発生します。

失敗例:

if (score >= 80) {
    System.out.println("合格");
} else {
    System.out.println("不合格");
}

もし scoreNaN だった場合、score >= 80false になるため、**「不合格」**と表示されます。一見正しく動いているように見えますが、本来計算不能なはずのデータが「不合格」として処理されてしまうため、後で原因不明のバグに繋がることがあります。

対策:

計算の途中で NaN が混じった可能性がある場合は、専用のメソッドでチェックする必要があります。

if (Double.isNaN(score)) {
    // エラー処理
} else if (score >= 80) {
    // 合格
}


まとめ

  • 1.0 > NaNfalse
  • 1.0 < NaNfalse
  • NaN == NaNfalse

先ほどの 0 * log(0) の結果をそのまま if 文などの条件分岐に使ってしまうと、予期せぬルートを通ってしまう危険があります。比較の前に isNaN() でガードをかけるか、計算の入り口で 0 を弾くのが安全です。


javaのDouble.compare()で数1とNaNを比較したら

実は、ここがJavaの面白い(そして少しややこしい)ところです。
単純な比較演算子(><)では「すべて false」でしたが、Double.compare(double d1, double d2) を使うと明確な順序が定義されます。

結論から言うと、Double.compare の世界では **NaNInfinity よりもさらに大きい「最大値」**として扱われます。


1. 実行結果

Double.compare(1.0, Double.NaN) を実行すると、結果は 負の整数(-1) になります。

int result = Double.compare(1.0, Double.NaN); 
// result は -1(第一引数の方が小さいという意味)

つまり、Javaの Double.compare における数値の大きさの順序は以下のようになります。

(小さい) $-\infty$ < 負の数 < $0.0$ < 正の数 < $+\infty$ < NaN (最大)


2. なぜ Double.compare は演算子と挙動が違うのか?

これには「コレクション(ListやMap)での扱い」という実用的な理由があります。

もし Double.compare も演算子と同じように「比較不能」を返してしまうと、double を含むリストを Collections.sort() で並べ替える際、アルゴリズムが壊れてしまいます。ソートや検索を行うためには、**「どんな値であっても一列に並べられる(全順序)」**必要があるため、Javaでは NaN を便宜上「一番大きい値」と決めているのです。


3. 注意: 0.0-0.0 の比較

Double.compare を使う際、もう一つ注意が必要なのが「ゼロ」の扱いです。

  • 演算子 (==): 0.0 == -0.0true
  • Double.compare: Double.compare(0.0, -0.0)1 (0.0 の方が大きいと判定)

これも NaN と同様、ソートなどの際に「値が厳密に異なるなら別の場所に配置する」ための仕様です。


まとめ:使い分けのポイント

比較方法 1.0 vs NaN 0.0 vs -0.0 用途
演算子 (<, >, ==) すべて false 等しい (true) 通常の数学的な条件分岐
Double.compare NaN が大きい 0.0 が大きい ソート、Mapのキー判定

Double.MIN_NORMALとDouble.MIN_VALUEの違いとは

Javaのdouble型において、この2つはどちらも「非常に小さな正の数」を指しますが、その**性質(精度)**が決定的に異なります。

一言で言うと、**「フルパワーで精度を保てる最小値」か、「精度を犠牲にしてでも0に粘る最小値」**かの違いです。


1. Double.MIN_NORMAL (正規化数の最小値)

浮動小数点数が、標準的な仕組み(正規化)を使ってフル精度(約15〜17桁)を維持できる最小の正の数です。

  • 値: $2^{-1022} \approx 2.22 \times 10^{-308}$
  • 特徴: これ以上の大きさであれば、doubleが持つ53ビットの有効桁数をすべて使って数値を表現できます。

2. Double.MIN_VALUE (非正規化数を含む最小値)

精度を落としてでも、0ではない数として表現できる限界の正の数です。

  • 値: $2^{-1074} \approx 4.9 \times 10^{-324}$
  • 特徴: MIN_NORMAL よりもさらに小さい領域(非正規化数といいます)に入ります。この領域では、0に近づくにつれて有効桁数がどんどん減っていき、最終的に「1桁の精度」しか残らなくなります。

比較まとめ

項目 Double.MIN_NORMAL Double.MIN_VALUE
およその値 $2.22 \times 10^{-308}$ $4.9 \times 10^{-324}$
精度の保証 100%維持(約15桁) 低下する(0に近いほどスカスカになる)
数学的名称 最小の正規化数 最小の非正規化数(正の最小値)

なぜ2種類あるのか?(アンダーフローの緩和)

もし MIN_NORMAL までしか数値が扱えないと、それより小さな計算結果はいきなり「ガクン」と 0 になってしまいます(突然の死)。これを防ぐために、精度を少しずつ犠牲にしながら 0 へと滑らかに繋げる「緩衝地帯」として MIN_VALUE までの領域が用意されています。

これを Gradual Underflow(段階的アンダーフロー) と呼びます。


開発での使い分け

  • **「計算精度が重要なループ」**を書く場合、MIN_NORMAL を下回る数値が頻発すると計算誤差が急増するため、警告や補正を入れる基準にするのが適切です。
  • **「とにかく 0 と区別したい」**という極限の判定であれば、MIN_VALUE を使います。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?