- 技術 💻
浮動小数点数とは|仕組み・誤差・0.1+0.2の理由を4言語で実演
結論
浮動小数点数とは、数を「符号・指数・仮数」に分け、2進数の決まった桁数で近似して表す方法です。桁数に限りがあるので、0.1 のような数は正確には入りません。
この記事でわかること
- 半精度〜倍精度とbf16・fp8の7形式の比較表
- 0.1+0.2を4言語で実行した結果とビット列
- 誤差の種類5つと、比較・お金の計算の直し方
10進数の 0.1 は、2進数にすると終わりのない小数になります。コンピューターはそれを決まった桁数で丸めてから、符号・指数・仮数の3つの欄に入れて保存します。
flowchart TD
A[10進数の 0.1] --> B[2進数にすると<br>0.000110011… と続く]
B --> C[53ビットで丸める]
C --> D[符号1・指数11・仮数52<br>の64ビットに入れる]
D --> E[実際の値は<br>0.1000000000000000055…]
急いでいる人向けに、よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| 浮動小数点数とは | 符号 × 仮数 × 2の指数乗 で表す数 | 浮動小数点数とは |
| 単精度・倍精度の違い | 32ビットで約7桁、64ビットで約16桁 | IEEE 754 の形式 |
| 0.1+0.2 が 0.3 にならない | 0.1 も 0.2 も2進数で割り切れないため | 0.1+0.2 |
| どう比較する? | == でなく許容誤差(isclose など) | 比較のやり方 |
| お金の計算は? | Decimal・BigDecimal か、円・銭の整数 | お金の計算 |
| float と double どちら? | 迷ったら double(倍精度) | 型の選び方 |
| bf16 とは | 単精度の上位16ビット。AI の学習で使う | 機械学習 |
| 誤差の種類 | 丸め誤差・桁落ち・情報落ちなど | 誤差の種類 |
確認環境:Ubuntu 24.04。0.1+0.2 とビット列の表示は Python 3.11.15・Node.js 22.22.2・OpenJDK 21.0.10・gcc 13.3.0 で実行しました。誤差・比較・お金の計算は Python 3.11.15(NumPy 2.4.4)、sum() の版による違いは Python 3.11.15・3.12.3・3.13.7、半精度・bfloat16・fp8・fp4 の値は Python 3.12.3 の NumPy 2.5.4 と ml_dtypes 0.6.0 で計算しました。規格・仕様は IEEE 754-2019 の規格ページ、Python・MDN・Java・PostgreSQL・NVIDIA・Google Cloud の公式ドキュメントで確かめました(確認日 2026年10月22日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「浮動小数点数とは|仕組み・誤差・0.1+0.2の理由を4言語で実演」
浮動小数点数とは(意味・読み方・なぜ「浮動」なのか)
この節では、浮動小数点数が何を表すもので、なぜその名前なのかが分かります。
浮動小数点数(ふどうしょうすうてんすう、floating-point number)は、数を「仮数 × 基数の指数乗」の形で表す方法です。 理科で使う「1.23 × 10の3乗」と同じ考え方で、コンピューターでは基数(何進数か)に2を使います。
| 10進数の値 | 10進数の指数表記 | 小数点の位置 |
|---|---|---|
| 1230 | 1.23 × 10の3乗 | 右へ3つ動く |
| 1.23 | 1.23 × 10の0乗 | 動かない |
| 0.00123 | 1.23 × 10のマイナス3乗 | 左へ3つ動く |
3つとも仮数は同じ 1.23 で、指数だけが違います。指数を変えると小数点の位置が動く(浮く)ので「浮動」小数点数と呼びます。 少ない桁で、とても大きな数(宇宙の距離)からとても小さな数(原子の大きさ)まで表せるのが利点です。
プログラミング言語の float・double、Python の float、JavaScript の Number は、どれも浮動小数点数です。ほぼすべてのコンピューターが、IEEE 754 という国際的な規格の形式を使っています。IEEE の規格ページによると、現行の版は IEEE 754-2019(2019年7月22日発行、Active)で、2進数と10進数の浮動小数点数の形式・演算・例外の扱いを定めています。
整数や固定小数点数との違い(ひとことで)
固定小数点数と浮動小数点数
「どこでも同じ精度」か「広い範囲」かの違い。詳しい比較は後の節
浮動小数点数の仕組み(符号部・指数部・仮数部)
この節では、64ビット(倍精度)や32ビット(単精度)の中に、数がどう詰められているかが分かります。
浮動小数点数のビットは、左から符号部・指数部・仮数部の3つに分かれています。単精度(32ビット)なら次のとおりです。
| 部分 | ビット数(単精度) | 入るもの |
|---|---|---|
| 符号部 | 1 | 0 なら正、1 なら負 |
| 指数部 | 8 | 2 の何乗かに、バイアス 127 を足した値 |
| 仮数部 | 23 | 「1.xxx」の小数点以下の部分 |
値は次の式で決まります(LaTeX を使わず文字で書きます)。
値 = (-1)の符号乗 × (1 + 仮数部 ÷ 2の23乗) × 2の(指数部 - 127)乗 … 単精度
値 = (-1)の符号乗 × (1 + 仮数部 ÷ 2の52乗) × 2の(指数部 - 1023)乗 … 倍精度
1正規化
2進数で「1.xxx × 2のn乗」の形にそろえること。10進数の 101.11 なら 1.0111 × 2の2乗。表し方が1通りに決まる。
2ケチ表現(隠れビット)
正規化すると先頭は必ず 1 なので、その 1 は保存しない。単精度は仮数部23ビットで、実際には24ビット分の精度になる。
3バイアス(げた履き)
指数はマイナスもあるので、決まった数を足して0以上にして保存する。単精度は127、倍精度は1023、半精度は15。
4特別な値
指数部が全部0ならゼロか非正規化数、全部1なら無限大か NaN。普通の数は指数部がその間の値。
基本情報技術者試験などで出る「ケチ表現」「げた履き表現」は、この2と3のことです。
IEEE 754 の形式(半精度・単精度・倍精度・四倍精度の違い)
この節では、ビット数ごとに、表せる範囲と精度がどう違うかが一覧で分かります。
| 形式(別名) | 符号・指数・仮数 | 10進の有効桁 | 最大値 |
|---|---|---|---|
| 倍精度 binary64(double) | 1・11・52 | 約15〜17桁 | 約 1.8 × 10の308乗 |
| 単精度 binary32(float) | 1・8・23 | 約6〜9桁 | 約 3.4 × 10の38乗 |
| 半精度 binary16(fp16) | 1・5・10 | 約3〜4桁 | 65504 |
| 四倍精度 binary128 | 1・15・112 | 約33〜36桁 | 約 1.19 × 10の4932乗 |
| bfloat16(bf16) | 1・8・7 | 約2〜3桁 | 約 3.39 × 10の38乗 |
| fp8 E4M3 | 1・4・3 | 約1桁 | 448 |
| fp8 E5M2 | 1・5・2 | 約1桁 | 57344 |
上の4つが IEEE 754 で定められた形式で、下の3つは機械学習のために作られた形式です(後の節で扱います)。NumPy と ml_dtypes の finfo で、それぞれの細かい値を出しました。
import numpy as np, ml_dtypes as md, importlib.metadata as m
print("numpy", np.__version__, "ml_dtypes", m.version("ml_dtypes"))
types = [("float64(倍精度)", np.float64), ("float32(単精度)", np.float32), ("float16(半精度)", np.float16),
("bfloat16", md.bfloat16), ("float8_e4m3fn", md.float8_e4m3fn), ("float8_e5m2", md.float8_e5m2)]
for name, t in types:
fi = md.finfo(t)
print(f"{name:16} bits={fi.bits:2} 指数={fi.nexp} 仮数={fi.nmant} 最大={float(fi.max):.6g} 最小正規={float(fi.smallest_normal):.6g} 最小非正規={float(fi.smallest_subnormal):.6g} eps={float(fi.eps):.6g} 10進桁={fi.precision}")
numpy 2.5.4 ml_dtypes 0.6.0
float64(倍精度) bits=64 指数=11 仮数=52 最大=1.79769e+308 最小正規=2.22507e-308 最小非正規=4.94066e-324 eps=2.22045e-16 10進桁=15
float32(単精度) bits=32 指数=8 仮数=23 最大=3.40282e+38 最小正規=1.17549e-38 最小非正規=1.4013e-45 eps=1.19209e-07 10進桁=6
float16(半精度) bits=16 指数=5 仮数=10 最大=65504 最小正規=6.10352e-05 最小非正規=5.96046e-08 eps=0.000976562 10進桁=3
bfloat16 bits=16 指数=8 仮数=7 最大=3.38953e+38 最小正規=1.17549e-38 最小非正規=9.18355e-41 eps=0.0078125 10進桁=2
float8_e4m3fn bits= 8 指数=4 仮数=3 最大=448 最小正規=0.015625 最小非正規=0.00195312 eps=0.125 10進桁=1
float8_e5m2 bits= 8 指数=5 仮数=2 最大=57344 最小正規=6.10352e-05 最小非正規=1.52588e-05 eps=0.25 10進桁=1
eps(マシンイプシロン)は「1 と、1 の次に大きい数との差」です。 倍精度は 2.22 × 10のマイナス16乗、単精度は 1.19 × 10のマイナス7乗で、これが「約16桁」「約7桁」の正体です。四倍精度は NumPy に型が無いので、規格の値(指数15ビット・仮数112ビット)から計算し、eps が 1.93 × 10のマイナス34乗、最大値が 1.19 × 10の4932乗になりました。
同じ数を各形式に入れると、どう変わるか
float64(倍精度) 0.1 0.3 0.3333333333333333 3.14159265358979 1000.1 70000.0
float32(単精度) 0.10000000149011612 0.30000001192092896 0.3333333432674408 3.1415927410125732 1000.0999755859375 70000.0
float16(半精度) 0.0999755859375 0.300048828125 0.333251953125 3.140625 1000.0 inf
bfloat16 0.10009765625 0.30078125 0.333984375 3.140625 1000.0 70144.0
float8_e4m3fn 0.1015625 0.3125 0.34375 3.25 nan nan
float8_e5m2 0.09375 0.3125 0.3125 3.0 1024.0 inf
倍精度の 0.1 を各形式に変換し、もう一度倍精度に戻して表示した値です。半精度は 70000 が入らず inf(無限大)になり、bfloat16 は 70000 が入る代わりに 70144 とずれます。 範囲と精度のどちらを優先するかで、形式が分かれていることが分かります。
整数はどこまで正確に表せるか
浮動小数点数でも、仮数部に収まる大きさまでの整数は正確に表せます。その上限を1つ超えた整数を入れた結果です。
| 形式 | 正確な整数の上限 | 上限+1 を入れた結果 |
|---|---|---|
| 倍精度 | 2の53乗 = 9007199254740992 | 9007199254740993 → 9007199254740992 |
| 単精度 | 2の24乗 = 16777216 | 16777217 → 16777216 |
| bfloat16 | 2の8乗 = 256 | 257 → 256 |
| 半精度 | 2の11乗 = 2048 | 2049 → 2048 |
JavaScript の数値はすべて倍精度なので、この上限がそのまま効きます。MDN によると Number.MAX_SAFE_INTEGER は 9007199254740991(2の53乗 − 1)で、それより大きい整数には BigInt を使います。
MAX_SAFE_INTEGER: 9007199254740991 2**53 + 1: 9007199254740992 JSON.parse: 9007199254740992
BigInt: 9007199254740993n
Node.js 22.22.2 で JSON.parse('{"id": 9007199254740993}').id を実行すると、ID の末尾が 2 に変わりました。大きな ID を JSON の数値で受け取ると、JavaScript では別の値になります。 API では ID を文字列で送るのが安全です。
10進数を浮動小数点数に変換する方法(手計算)
この節では、10進数の 5.75 を単精度のビット列にする手順と、逆にビット列から値を戻す方法が分かります。
整数部と小数部を2進数にする
5 は 101。0.75 は2倍して整数部を取る:0.75×2=1.5 → 1、0.5×2=1.0 → 1。合わせて 101.11。
正規化する
小数点を左に2つ動かして 1.0111 × 2の2乗。先頭が 1 になる形にそろえる。
符号部を決める
正の数なので 0。負の数なら 1 にする。
指数部にバイアスを足す
2 + 127 = 129。8ビットの2進数で 10000001。
仮数部を23ビットにする
1.0111 の先頭の 1 を省き(ケチ表現)、0111 の後ろを 0 で埋めて23ビットにする。
3つを並べて検算する
0 10000001 01110000000000000000000。16進数で 40B80000。Python の struct で同じになるかを確かめる。
手計算の結果を、Python の struct(数をバイト列にする標準ライブラリ)で確かめました。−6.25 や 0.1 も並べています。
import struct
from fractions import Fraction
def f32_bits(v):
b = struct.unpack(">I", struct.pack(">f", v))[0]
s = f"{b:032b}"
return b, s[0], s[1:9], s[9:]
def decode32(b):
sign = b >> 31; e = (b >> 23) & 0xFF; frac = b & 0x7FFFFF
if e == 0: # 非正規化数
return (-1)**sign * Fraction(frac, 2**23) * Fraction(1, 2**126), sign, e, frac
return (-1)**sign * (1 + Fraction(frac, 2**23)) * Fraction(2)**(e - 127), sign, e, frac
print("[2] 10進数 → 単精度(binary32)のビット列")
for v in [5.75, -6.25, 0.1, 0.3, 1.0, 0.5]:
b, s, e, m = f32_bits(v)
print(f" {v:>6} 符号 {s} 指数 {e} ({int(e,2)}-127={int(e,2)-127:+d}) 仮数 {m} = 0x{b:08X}")
print("\n[3] ビット列 → 値(式: (-1)^符号 × (1 + 仮数/2^23) × 2^(指数-127))")
for b in [0x40B80000, 0x3DCCCCCD, 0x00000001, 0x7F7FFFFF]:
val, sign, e, frac = decode32(b)
print(f" 0x{b:08X}: 符号 {sign} 指数 {e} 仮数 {frac} → {float(val):.10g} (正確には {val if val.denominator < 10**12 else format(float(val), '.30g')})")
[2] 10進数 → 単精度(binary32)のビット列
5.75 符号 0 指数 10000001 (129-127=+2) 仮数 01110000000000000000000 = 0x40B80000
-6.25 符号 1 指数 10000001 (129-127=+2) 仮数 10010000000000000000000 = 0xC0C80000
0.1 符号 0 指数 01111011 (123-127=-4) 仮数 10011001100110011001101 = 0x3DCCCCCD
0.3 符号 0 指数 01111101 (125-127=-2) 仮数 00110011001100110011010 = 0x3E99999A
1.0 符号 0 指数 01111111 (127-127=+0) 仮数 00000000000000000000000 = 0x3F800000
0.5 符号 0 指数 01111110 (126-127=-1) 仮数 00000000000000000000000 = 0x3F000000
[3] ビット列 → 値(式: (-1)^符号 × (1 + 仮数/2^23) × 2^(指数-127))
0x40B80000: 符号 0 指数 129 仮数 3670016 → 5.75 (正確には 23/4)
0x3DCCCCCD: 符号 0 指数 123 仮数 5033165 → 0.1000000015 (正確には 13421773/134217728)
0x00000001: 符号 0 指数 0 仮数 1 → 1.401298464e-45 (正確には 1.40129846432481707092372958329e-45)
0x7F7FFFFF: 符号 0 指数 254 仮数 8388607 → 3.402823466e+38 (正確には 340282346638528859811704183484516925440)
5.75 は手計算どおり 0x40B80000 になり、逆に戻すと 23/4 = 5.75 ちょうどです。 一方、0.1 は仮数部の最後が …1101 で、戻すと 0.1000000015 になりました。0.1 は単精度では正確に入らず、少し大きい値に丸められていることが分かります。0x00000001 は単精度で表せる最小の正の数(非正規化数)、0x7F7FFFFF は最大の数です。16進数とビットの読み方は、関連記事「16進数とは|変換・計算のやり方と0〜255早見表を3言語で実演」で扱っています。
0.1 を2進数にすると割り切れない理由
この節では、0.1 が2進数で終わらない理由と、コンピューターに入っている 0.1 の本当の値が分かります。
10進数の小数を2進数にするには、「2倍して整数部を取り出す」をくり返します。0.1 を分数のまま計算した結果です。
[1] 0.1 を2進数にする(2倍して整数部を取り出す)
1回目 1/5 → 整数部 0 残り 1/5
2回目 2/5 → 整数部 0 残り 2/5
3回目 4/5 → 整数部 0 残り 4/5
4回目 8/5 → 整数部 1 残り 3/5
5回目 6/5 → 整数部 1 残り 1/5
6回目 2/5 → 整数部 0 残り 2/5
7回目 4/5 → 整数部 0 残り 4/5
8回目 8/5 → 整数部 1 残り 3/5
9回目 6/5 → 整数部 1 残り 1/5
10回目 2/5 → 整数部 0 残り 2/5
11回目 4/5 → 整数部 0 残り 4/5
12回目 8/5 → 整数部 1 残り 3/5
0.1 = 0.000110011001…(0011 が繰り返す)
5回目で残りが 1/5 に戻り、同じ計算がくり返されます。0.1 = 1/10 で、分母の 10 に 5 という素因数があるため、2進数では割り切れません。 10進数で 1/3 = 0.333… が終わらないのと同じ理由です。逆に、0.5(1/2)・0.25(1/4)・0.75(3/4)のように分母が2のべき乗の数は、2進数で正確に表せます。
| 10進数 | 2進数 | 正確に入るか |
|---|---|---|
| 0.5 | 0.1 | 入る(1/2) |
| 0.75 | 0.11 | 入る(3/4) |
| 0.1 | 0.000110011…(0011 の繰り返し) | 入らない |
| 0.2 | 0.00110011…(0011 の繰り返し) | 入らない |
| 0.3 | 0.0100110011…(1001 の繰り返し) | 入らない |
Python のドキュメント「浮動小数点演算、その問題と制限」によると、倍精度の 0.1 は分数 3602879701896397 ÷ 2の55乗 で近似されます。それを10進数で正確に書いたのが次の値で、Python の Decimal(0.1)、JavaScript の toPrecision(55)、Java の new BigDecimal(0.1)、C の printf("%.55f") のどれでも同じ値が出ました。
0.1000000000000000055511151231257827021181583404541015625
print(0.1) で 0.1 と表示されるのは、Python や JavaScript が「元の値に戻せるいちばん短い10進数」で表示するためで、中身は上の値です。
0.1+0.2 が 0.30000000000000004 になる理由(4言語で実行)
この節では、0.1 + 0.2 を Python・JavaScript・Java・C で実行した結果と、ずれの正体がビット単位で分かります。
Python
import struct, sys
from decimal import Decimal
print(sys.version.split()[0])
a = 0.1 + 0.2
print(a) # 表示
print(a == 0.3) # 比較
print(repr(0.3), f"{a:.20f}", f"{0.3:.20f}")
print(Decimal(0.1)) # 0.1 の本当の値
print(Decimal(0.2))
print(Decimal(0.3))
print(Decimal(a))
def bits64(x):
b = struct.unpack(">Q", struct.pack(">d", x))[0]
s = f"{b:064b}"
return f"{s[0]} {s[1:12]} {s[12:]} 0x{b:016x}"
for name, x in [("0.1", 0.1), ("0.2", 0.2), ("0.1+0.2", a), ("0.3", 0.3)]:
print(f"{name:8} {bits64(x)} {x.hex()}")
3.11.15
0.30000000000000004
False
0.3 0.30000000000000004441 0.29999999999999998890
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.299999999999999988897769753748434595763683319091796875
0.3000000000000000444089209850062616169452667236328125
0.1 0 01111111011 1001100110011001100110011001100110011001100110011010 0x3fb999999999999a 0x1.999999999999ap-4
0.2 0 01111111100 1001100110011001100110011001100110011001100110011010 0x3fc999999999999a 0x1.999999999999ap-3
0.1+0.2 0 01111111101 0011001100110011001100110011001100110011001100110100 0x3fd3333333333334 0x1.3333333333334p-2
0.3 0 01111111101 0011001100110011001100110011001100110011001100110011 0x3fd3333333333333 0x1.3333333333333p-2
JavaScript(Node.js)
console.log(process.version)
const a = 0.1 + 0.2
console.log(a)
console.log(a === 0.3)
console.log(a.toFixed(20), (0.3).toFixed(20))
console.log(0.1.toPrecision(55))
function bits64(x) {
const v = new DataView(new ArrayBuffer(8)); v.setFloat64(0, x)
const s = v.getBigUint64(0).toString(2).padStart(64, "0")
return `${s[0]} ${s.slice(1, 12)} ${s.slice(12)} 0x${v.getBigUint64(0).toString(16).padStart(16, "0")}`
}
for (const [n, x] of [["0.1", 0.1], ["0.2", 0.2], ["0.1+0.2", a], ["0.3", 0.3]]) console.log(n.padEnd(8), bits64(x))
console.log(Math.abs(a - 0.3), Number.EPSILON)
v22.22.2
0.30000000000000004
false
0.30000000000000004441 0.29999999999999998890
0.1000000000000000055511151231257827021181583404541015625
0.1 0 01111111011 1001100110011001100110011001100110011001100110011010 0x3fb999999999999a
0.2 0 01111111100 1001100110011001100110011001100110011001100110011010 0x3fc999999999999a
0.1+0.2 0 01111111101 0011001100110011001100110011001100110011001100110100 0x3fd3333333333334
0.3 0 01111111101 0011001100110011001100110011001100110011001100110011 0x3fd3333333333333
5.551115123125783e-17 2.220446049250313e-16
Java
public class Add {
public static void main(String[] args) {
System.out.println(System.getProperty("java.version"));
double a = 0.1 + 0.2;
System.out.println(a);
System.out.println(a == 0.3);
float f = 0.1f + 0.2f;
System.out.println(f);
System.out.println(f == 0.3f);
System.out.println(new java.math.BigDecimal(0.1));
System.out.println(new java.math.BigDecimal(f));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(a)));
System.out.println(Long.toHexString(Double.doubleToLongBits(a)) + " " + Long.toHexString(Double.doubleToLongBits(0.3)));
System.out.println(Integer.toHexString(Float.floatToIntBits(0.1f)) + " " + Integer.toHexString(Float.floatToIntBits(f)) + " " + Integer.toHexString(Float.floatToIntBits(0.3f)));
System.out.println(Double.toHexString(a));
System.out.println(Math.ulp(0.3));
}
}
21.0.10
0.30000000000000004
false
0.3
true
0.1000000000000000055511151231257827021181583404541015625
0.300000011920928955078125
11111111010011001100110011001100110011001100110011001100110100
3fd3333333333334 3fd3333333333333
3dcccccd 3e99999a 3e99999a
0x1.3333333333334p-2
5.551115123125783E-17
java Add.java で、コンパイルせずに1ファイルのまま実行しています。Long.toBinaryString は先頭の0(符号部)を出さないので、63桁になっています。
C
#include <stdio.h>
#include <string.h>
#include <stdint.h>
int main(void) {
double a = 0.1 + 0.2;
printf("%g\n", a); /* 既定の6桁 */
printf("%.17g\n", a); /* 17桁 */
printf("%d\n", a == 0.3);
printf("%.55f\n", 0.1);
printf("%a\n", a);
float f = 0.1f + 0.2f;
printf("%.9g %d\n", f, f == 0.3f);
uint64_t u; memcpy(&u, &a, sizeof u);
printf("0x%016llx\n", (unsigned long long)u);
return 0;
}
0.3
0.30000000000000004
0
0.1000000000000000055511151231257827021181583404541015625
0x1.3333333333334p-2
0.300000012 1
0x3fd3333333333334
gcc -O0 -Wall -o add add.c && ./add で実行しました。C の %g は6桁で丸めて 0.3 と表示しますが、%.17g にすると同じ 0.30000000000000004 が出ます。 表示の桁数が違うだけで、中身は4言語とも同じです。C 言語のコンパイルと実行の基本は、関連記事「C言語とは|入門から環境構築・ポインタ・mallocまで実行例で解説」で扱っています。
ずれの正体:丸めが2回重なる
ビット列を並べると、何が起きたかが分かります。
- 0.1 と 0.2 は、どちらも本当の値より少し大きく丸められています(仮数部の末尾が
…1010、Decimalで見ると 0.1000…0555 と 0.2000…111)。 - 足した結果も53ビットに収まらないので、もう一度丸められ、仮数部の末尾が
…0100(16進数で …334)になりました。 - 一方、0.3 を直接入れると、本当の値より少し小さい側に丸められ、末尾は
…0011(…333)です。 - 2つはちょうど隣り合った浮動小数点数で、差は 5.55 × 10のマイナス17乗(0.3 付近の ULP。ULP は隣の数との間隔)でした。
単精度では、0.1f + 0.2f と 0.3f がどちらも 0x3e99999a になり、== が true になりました。単精度のほうが精度が高いわけではありません。 丸めの結果がたまたま同じ数に落ちただけで、== で比べてよいかは数しだいです。だから、浮動小数点数は == ではなく許容誤差で比べます(後の節)。
浮動小数点数のビット列を表示するツール(コピペ用)
この節では、任意の数を半精度・単精度・倍精度のビット列に分解して表示する Python の関数が手に入ります。
import struct
FORMATS = { # 名前: (struct の型, 全ビット, 指数のビット, 仮数のビット)
"half": ("e", 16, 5, 10),
"single": ("f", 32, 8, 23),
"double": ("d", 64, 11, 52),
}
def float_bits(x: float, fmt: str = "double") -> None:
code, total, ebits, mbits = FORMATS[fmt]
raw = int.from_bytes(struct.pack(">" + code, x), "big")
s = f"{raw:0{total}b}"
sign, exp, man = s[0], s[1:1 + ebits], s[1 + ebits:]
bias = 2 ** (ebits - 1) - 1
e = int(exp, 2)
stored = struct.unpack(">" + code, raw.to_bytes(total // 8, "big"))[0]
if e == 0:
kind = "ゼロ" if int(man, 2) == 0 else "非正規化数"
elif e == 2 ** ebits - 1:
kind = "無限大" if int(man, 2) == 0 else "NaN"
else:
kind = f"正規化数 指数 {e}-{bias}={e - bias:+d}"
print(f"{fmt:6} {x!r:>8} → {sign} | {exp} | {man} 0x{raw:0{total // 4}X} {kind} 実際の値 {stored!r}")
if __name__ == "__main__":
for v in [0.1, -6.25, 65504.0, 1e-7]:
for f in ("half", "single", "double"):
float_bits(v, f)
float_bits(float("inf"), "single"); float_bits(float("nan"), "single"); float_bits(-0.0, "single")
try:
float_bits(70000.0, "half")
except OverflowError as err:
print("half 70000.0 → OverflowError:", err)
half 0.1 → 0 | 01011 | 1001100110 0x2E66 正規化数 指数 11-15=-4 実際の値 0.0999755859375
single 0.1 → 0 | 01111011 | 10011001100110011001101 0x3DCCCCCD 正規化数 指数 123-127=-4 実際の値 0.10000000149011612
double 0.1 → 0 | 01111111011 | 1001100110011001100110011001100110011001100110011010 0x3FB999999999999A 正規化数 指数 1019-1023=-4 実際の値 0.1
half -6.25 → 1 | 10001 | 1001000000 0xC640 正規化数 指数 17-15=+2 実際の値 -6.25
single -6.25 → 1 | 10000001 | 10010000000000000000000 0xC0C80000 正規化数 指数 129-127=+2 実際の値 -6.25
double -6.25 → 1 | 10000000001 | 1001000000000000000000000000000000000000000000000000 0xC019000000000000 正規化数 指数 1025-1023=+2 実際の値 -6.25
half 65504.0 → 0 | 11110 | 1111111111 0x7BFF 正規化数 指数 30-15=+15 実際の値 65504.0
single 65504.0 → 0 | 10001110 | 11111111110000000000000 0x477FE000 正規化数 指数 142-127=+15 実際の値 65504.0
double 65504.0 → 0 | 10000001110 | 1111111111000000000000000000000000000000000000000000 0x40EFFC0000000000 正規化数 指数 1038-1023=+15 実際の値 65504.0
half 1e-07 → 0 | 00000 | 0000000010 0x0002 非正規化数 実際の値 1.1920928955078125e-07
single 1e-07 → 0 | 01100111 | 10101101011111110010101 0x33D6BF95 正規化数 指数 103-127=-24 実際の値 1.0000000116860974e-07
double 1e-07 → 0 | 01111100111 | 1010110101111111001010011010101111001010111101001000 0x3E7AD7F29ABCAF48 正規化数 指数 999-1023=-24 実際の値 1e-07
single inf → 0 | 11111111 | 00000000000000000000000 0x7F800000 無限大 実際の値 inf
single nan → 0 | 11111111 | 10000000000000000000000 0x7FC00000 NaN 実際の値 nan
single -0.0 → 1 | 00000000 | 00000000000000000000000 0x80000000 ゼロ 実際の値 -0.0
half 70000.0 → OverflowError: float too large to pack with e format
読み取れることです。
- 0.1 の指数は3形式とも −4 で、違うのは仮数部の長さだけです。 仮数部が短いほど、実際の値が 0.1 から離れます(半精度 0.09998、単精度 0.10000000149)。
- 65504 は半精度の最大値で、指数部は 11110、仮数部は全部 1 です。指数部が全部 1(11111)は無限大と NaN 用にとってあります。
- 1e-7 は半精度の正規化数の範囲より小さいので、指数部が全部 0 の非正規化数になり、1.19 × 10のマイナス7乗とかなりずれました。
structの"e"は半精度の形式で、範囲を超えるとOverflowError: float too large to pack with e formatで止まります。
他の言語では、JavaScript は DataView の setFloat64・setFloat32、Java は Double.doubleToLongBits・Float.floatToIntBits、C は memcpy で整数にコピーする方法でビット列を取り出せます(前の節のコード)。Java の API ドキュメントによると、Java 20 以降は Float.floatToFloat16 で半精度(binary16)のビット列も得られます。
浮動小数点数の誤差の種類(丸め誤差・桁落ち・情報落ち)
この節では、浮動小数点数で起きる誤差の種類と、それぞれが実際にどれくらいの大きさになるかが分かります。
1丸め誤差
決まった桁数に収めるとき、はみ出した部分を丸めて生じる誤差。0.1 を入れた時点で起きている。計算をくり返すと積み重なる。
2情報落ち
大きい数に小さい数を足すと、小さい数が仮数部からはみ出して消える。1e16 + 1 が 1e16 のままになる。
3桁落ち
ほぼ同じ大きさの数を引くと、上の桁が打ち消し合い、信頼できる桁が急に減る。式の変形で避けられることが多い。
4オーバーフロー・アンダーフロー
最大値を超えると無限大、最小値より小さいと非正規化数を経て 0 になる。半精度・fp8 では普通の値でも起きる。
上の4つに加えて、計算の順番で答えが変わる(結合法則が成り立たない)ことも、浮動小数点数の特徴です。Python 3.11.15 と NumPy 2.4.4 で、それぞれを実際に起こしました。
import math, sys
import numpy as np
print("Python", sys.version.split()[0], "numpy", np.__version__)
print("[A] 丸め誤差の累積:0.1 を n 回足す")
for n in (10, 1000, 1_000_000):
s64 = 0.0
for _ in range(n): s64 += 0.1
s32 = np.float32(0)
one = np.float32(0.1)
for _ in range(n): s32 = np.float32(s32 + one)
print(f" n={n:>9,} float64 {s64!r:<22} float32 {float(s32)!r:<20} 正しい値 {n/10:g}")
print("[B] ループの条件に == を使うと止まらない")
x = 0.0; hit = False
for i in range(20):
x += 0.1
if x == 1.0: hit = True
if i in (8, 9, 10): print(f" {i+1}回目 x = {x!r}")
print(" x == 1.0 になった回数:", int(hit))
print("[C] 情報落ち:大きい数に小さい数を足すと消える")
print(" 1e16 + 1 =", 1e16 + 1, " (1e16 + 1) - 1e16 =", (1e16 + 1) - 1e16)
print(" float32: 16777216 + 1 =", float(np.float32(16777216) + np.float32(1)))
big = 1e16
for _ in range(1000): big += 1.0
print(" 1e16 に 1.0 を1000回足す →", big - 1e16)
print(" 先に1000を作ってから足す →", (1e16 + 1000.0) - 1e16)
print("[D] 桁落ち:近い数どうしの引き算")
for xv in (1e6, 1e12, 1e15):
naive = math.sqrt(xv + 1) - math.sqrt(xv)
better = 1 / (math.sqrt(xv + 1) + math.sqrt(xv))
print(f" x={xv:g} √(x+1)-√x = {naive!r:<24} 1/(√(x+1)+√x) = {better!r}")
xs = 1e-8
print(f" 1-cos(1e-8) = {1 - math.cos(xs)!r} 2*sin(1e-8/2)**2 = {2*math.sin(xs/2)**2!r}")
a, b, c = 1.0, 1e8, 1.0
r_naive = (-b + math.sqrt(b*b - 4*a*c)) / (2*a)
q = -0.5 * (b + math.copysign(math.sqrt(b*b - 4*a*c), b))
print(f" x^2 + 1e8 x + 1 = 0 の小さい解: 公式そのまま {r_naive!r} 式変形 {c/q!r}")
print("[E] 足す順番で結果が変わる(結合法則が成り立たない)")
print(" (0.1 + 0.2) + 0.3 =", (0.1 + 0.2) + 0.3)
print(" 0.1 + (0.2 + 0.3) =", 0.1 + (0.2 + 0.3))
vals = [1e16, 1.0, -1e16]
print(" [1e16, 1, -1e16] 左から足す:", (vals[0] + vals[1]) + vals[2], " 並べ替え:", (vals[0] + vals[2]) + vals[1])
print("[F] 合計の関数の違い")
data = [0.1] * 10
print(" sum([0.1]*10) =", sum(data))
print(" math.fsum([0.1]*10) =", math.fsum(data))
print(" sum([1e16,1,-1e16]) =", sum(vals), " math.fsum =", math.fsum(vals))
print(" np.sum(float32 0.1 を1000万個) =", float(np.full(10_000_000, 0.1, dtype=np.float32).sum()), " ループ(float32)は[A]")
print("[G] オーバーフロー・アンダーフロー")
print(" 1e308 * 10 =", 1e308 * 10)
print(" sys.float_info.max =", sys.float_info.max)
print(" 5e-324 / 2 =", 5e-324 / 2, " 2.2250738585072014e-308 / 2 =", 2.2250738585072014e-308 / 2)
丸め誤差は、くり返すと積み重なる
Python 3.11.15 numpy 2.4.4
[A] 丸め誤差の累積:0.1 を n 回足す
n= 10 float64 0.9999999999999999 float32 1.0000001192092896 正しい値 1
n= 1,000 float64 99.9999999999986 float32 99.9990463256836 正しい値 100
n=1,000,000 float64 100000.00000133288 float32 100958.34375 正しい値 100000
[B] ループの条件に == を使うと止まらない
9回目 x = 0.8999999999999999
10回目 x = 0.9999999999999999
11回目 x = 1.0999999999999999
x == 1.0 になった回数: 0
単精度で 0.1 を100万回足すと 100958.34 になり、正しい値から 958 もずれました。 倍精度のずれは 0.0000013 です。合計が大きくなるほど1回の足し算で丸められる量が増えるため、単精度では誤差が急に大きくなります。
[B] のように、0.1 ずつ増やして x == 1.0 で止めるループは、x が 0.9999999999999999 から 1.0999999999999999 に飛ぶので止まりません。ループの回数は整数で数え、小数はそこから計算します(x = i * 0.1 か x = i / 10)。
情報落ちと桁落ち
[C] 情報落ち:大きい数に小さい数を足すと消える
1e16 + 1 = 1e+16 (1e16 + 1) - 1e16 = 0.0
float32: 16777216 + 1 = 16777216.0
1e16 に 1.0 を1000回足す → 0.0
先に1000を作ってから足す → 1000.0
[D] 桁落ち:近い数どうしの引き算
x=1e+06 √(x+1)-√x = 0.0004999998750463419 1/(√(x+1)+√x) = 0.0004999998750000625
x=1e+12 √(x+1)-√x = 5.00003807246685e-07 1/(√(x+1)+√x) = 4.999999999998749e-07
x=1e+15 √(x+1)-√x = 1.862645149230957e-08 1/(√(x+1)+√x) = 1.5811388300841893e-08
1-cos(1e-8) = 0.0 2*sin(1e-8/2)**2 = 5.0000000000000005e-17
x^2 + 1e8 x + 1 = 0 の小さい解: 公式そのまま -7.450580596923828e-09 式変形 -1e-08
- 情報落ち:1e16 付近の倍精度の数は、隣の数との間隔(ULP)が 2 です。1 を足しても半分に届かないので丸めで消え、1000回足しても 0 のままでした。小さい数どうしを先に足してから大きい数に足すと、1000 が残ります。
- 桁落ち:x = 10の15乗 のとき、√(x+1) − √x をそのまま計算すると 1.86 × 10のマイナス8乗 で、正しい値 1.58 × 10のマイナス8乗 と2割近くずれました。分子と分母に √(x+1) + √x を掛けて 1 ÷ (√(x+1) + √x) に変形すると、引き算が無くなり正しい値が出ます。
- 1 − cos(10のマイナス8乗) は 0 になりましたが、同じ値を 2 × sin(x/2) の2乗 と変形すると 5 × 10のマイナス17乗 が出ます。2次方程式の解の公式も、b が大きいと小さい方の解が −7.45 × 10のマイナス9乗 とずれ、「c ÷ もう一方の解」で求めると −1 × 10のマイナス8乗 になりました。
足す順番で結果が変わる・合計の関数
[E] 足す順番で結果が変わる(結合法則が成り立たない)
(0.1 + 0.2) + 0.3 = 0.6000000000000001
0.1 + (0.2 + 0.3) = 0.6
[1e16, 1, -1e16] 左から足す: 0.0 並べ替え: 1.0
[F] 合計の関数の違い
sum([0.1]*10) = 0.9999999999999999
math.fsum([0.1]*10) = 1.0
sum([1e16,1,-1e16]) = 0.0 math.fsum = 1.0
np.sum(float32 0.1 を1000万個) = 1000000.125 ループ(float32)は[A]
数学では (a + b) + c = a + (b + c) ですが、浮動小数点数では成り立ちません。 並列で合計する(GPU・複数スレッド・データベースの集計)と、足す順番が実行ごとに変わり、最後の桁が変わることがあります。
math.fsum は、途中で失われた桁を追いかけて、丸めを最後の1回だけにする関数です(Python のドキュメント)。NumPy の sum は2つずつ組にして足す方法(ペアワイズ加算)を使うので、単精度の 0.1 を1000万個足しても 1000000.125 で、1つずつ足すループ(100万回で 958 のずれ)よりずっと正確でした。
Python 3.12 で sum() の結果が変わった
同じ式を Python の版を変えて実行しました。
3.11.15 0.9999999999999999 0.0
3.12.3 1.0 1.0
3.13.7 1.0 1.0
左から版、sum([0.1]*10)、sum([1e16, 1.0, -1e16]) です。Python 3.12 の「What's New」に、sum() が浮動小数点数の合計に Neumaier の加算(失われた分を別に覚えておく方法)を使うようになったと書かれています。同じコードでも、Python 3.11 と 3.12 以降で合計の最後の桁が変わることがあります。 テストで合計値を == で比べていると、版を上げたときに失敗する原因になります。
オーバーフローとアンダーフロー
[G] オーバーフロー・アンダーフロー
1e308 * 10 = inf
sys.float_info.max = 1.7976931348623157e+308
5e-324 / 2 = 0.0 2.2250738585072014e-308 / 2 = 1.1125369292536007e-308
倍精度の最大値(約 1.8 × 10の308乗)を超えた掛け算は、エラーにならず inf になりました。最小の正規化数 2.2250738585072014 × 10のマイナス308乗 を2で割ると、精度を少しずつ失いながら非正規化数として残り、最小の数 5 × 10のマイナス324乗 を2で割ると 0 になります。
NaN・Infinity・-0(特別な値)の扱い
この節では、NaN(非数)・無限大・マイナスゼロがどんな値で、各言語でどう振る舞うかが分かります。
| 値 | 倍精度のビット列(16進数) | できる計算の例 |
|---|---|---|
| +Infinity(無限大) | 7ff0000000000000 | 1.0 ÷ 0.0、最大値を超える計算 |
| −Infinity | fff0000000000000 | −1.0 ÷ 0.0 |
| NaN(非数) | 7ff8000000000000(の一例) | 0.0 ÷ 0.0、∞ − ∞、√(−1) |
| −0.0(マイナスゼロ) | 8000000000000000 | −1e−300 × 1e−300、−0.0 そのもの |
ビット列は Python の struct で出した値です。4言語で同じ操作をした結果をまとめます。
| 操作 | Python | JavaScript | Java・C |
|---|---|---|---|
| 1.0 ÷ 0.0 | ZeroDivisionError | Infinity | Infinity(C は inf) |
| 0.0 ÷ 0.0 | ZeroDivisionError | NaN | NaN(C は -nan と表示) |
| NaN == NaN | False | false | false(C は 0) |
| −0.0 == 0.0 | True | true | true(C は 1) |
| 1 ÷ −0.0 | ZeroDivisionError | -Infinity | -Infinity(C は -inf) |
| NaN の判定 | math.isnan | Number.isNaN | Double.isNaN・isnan |
# Python 3.11.15
nan == nan: False math.isnan: True
inf - inf: nan inf * 0: nan inf > 1e308: True
-0.0 == 0.0: True str: -0.0 copysign(1,-0.0): -1.0
sorted([3, nan, 1, 2]): [3.0, nan, 1.0, 2.0]
max(nan, 1): nan max(1, nan): 1.0
1.0 / 0.0 → ZeroDivisionError: float division by zero
math.sqrt(-1) → ValueError: math domain error
math.exp(1000) → OverflowError: math range error
np: 1/0 = inf 0/0 = nan sqrt(-1) = nan
# Node.js 22.22.2
NaN === NaN: false Number.isNaN: true Object.is: true
-0 === 0: true Object.is(-0, 0): false 1/-0: -Infinity String(-0): 0 JSON: 0 null null
[3, NaN, 1, 2].sort((a,b)=>a-b): [ 3, NaN, 1, 2 ]
Math.max(NaN, 1): NaN isNaN('abc'): true Number.isNaN('abc'): false
# Java 21.0.10
NaN == NaN: false Double.isNaN: true Double.valueOf(NaN).equals(NaN): true
-0.0 == 0.0: true Double.compare(-0.0, 0.0): -1 1/-0.0: -Infinity
Arrays.sort: [-0.0, 0.0, 1.0, 2.0, 3.0, NaN]
Math.max(NaN, 1): NaN (int) NaN: 0 (int) 1e20: 2147483647
# C(gcc 13.3.0)
1.0/0.0 = inf FE_DIVBYZERO=1
0.0/0.0 = -nan isnan=1 n==n: 0 FE_INVALID=1
-0.0 == 0.0: 1 signbit(-0.0)=1 1/-0.0=-inf
覚えておくと事故を防げることです。
- NaN は自分自身とも等しくありません。
x == xが false になるのは NaN だけなので、判定はmath.isnan・Number.isNaNを使います。JavaScript のisNaN('abc')は文字列を数に変換してから調べるので true になり、Number.isNaN('abc')は false です。 - NaN が混ざると、並べ替えと最大値が壊れます。 Python の
sortedと JavaScript のsortは並べ替えずに返し、Python のmax(nan, 1)とmax(1, nan)は引数の順番で答えが変わりました。Java のArrays.sortは NaN を最後に、−0.0 を 0.0 の前に置きます。 - Python の float は 0 で割るとエラーになります。 IEEE 754 の既定の動作は無限大か NaN を返すことで、JavaScript・Java・C と NumPy はそうなりました(NumPy は
RuntimeWarning: divide by zero encountered in scalar divideなどの警告を出します)。 - Java で NaN や大きすぎる数を int に変換すると、エラーにならず 0 や 2147483647 になります。 Python の
int(float('nan'))はValueError: cannot convert float NaN to integerで止まります。 - JSON に NaN と Infinity はありません。 JavaScript の
JSON.stringifyはnullに変えるので、受け取った側で値が消えます。 - −0.0 は
==では 0.0 と等しいものの、1 を割ると −Infinity になり、表示も Python と Java では-0.0です。区別するときはmath.copysign・Object.is・Double.compare・signbitを使います。
浮動小数点数の比較の正しいやり方(許容誤差)
この節では、浮動小数点数どうしを比べるときの書き方と、よくある失敗が分かります。
結論:== ではなく「差が許容誤差より小さいか」で比べ、許容誤差は数の大きさに合わせます(相対誤差)。0 と比べるときだけは絶対誤差も使います。
import math, struct
import numpy as np
a = 0.1 + 0.2
print("[1] math.isclose の既定(rel_tol=1e-09, abs_tol=0.0)")
print(" isclose(0.1+0.2, 0.3) ", math.isclose(a, 0.3))
print(" isclose(1e-20, 0.0) ", math.isclose(1e-20, 0.0))
print(" isclose(1e-20, 0.0, abs_tol=1e-12)", math.isclose(1e-20, 0.0, abs_tol=1e-12))
print(" isclose(1000.1+1000.2, 2000.3) ", math.isclose(1000.1 + 1000.2, 2000.3))
print("[2] Number.EPSILON(=2^-52)で固定の差を見ると、大きい数で失敗する")
eps = 2**-52
for x, y in [(0.1 + 0.2, 0.3), (1000.1 + 1000.2, 2000.3)]:
print(f" {x!r:<22} vs {y!r:<9} 差 {abs(x - y):.3e} 差<EPSILON: {abs(x - y) < eps} isclose: {math.isclose(x, y)}")
print("[3] numpy.isclose の既定(rtol=1e-05, atol=1e-08)は緩め・非対称")
print(" np.isclose(1e-9, 0) =", bool(np.isclose(1e-9, 0)), " np.isclose(100.0, 100.0009) =", bool(np.isclose(100.0, 100.0009)))
print("[4] ULP(隣の数との間隔)")
for x in (1.0, 1000.0, 1e16):
print(f" math.ulp({x:g}) = {math.ulp(x)!r} nextafter = {math.nextafter(x, math.inf)!r}")
def ulps(x, y):
i = lambda v: struct.unpack(">q", struct.pack(">d", v))[0]
return abs(i(x) - i(y))
print(" 0.1+0.2 と 0.3 は", ulps(a, 0.3), "ULP 離れている")
[1] math.isclose の既定(rel_tol=1e-09, abs_tol=0.0)
isclose(0.1+0.2, 0.3) True
isclose(1e-20, 0.0) False
isclose(1e-20, 0.0, abs_tol=1e-12) True
isclose(1000.1+1000.2, 2000.3) True
[2] Number.EPSILON(=2^-52)で固定の差を見ると、大きい数で失敗する
0.30000000000000004 vs 0.3 差 5.551e-17 差<EPSILON: True isclose: True
2000.3000000000002 vs 2000.3 差 2.274e-13 差<EPSILON: False isclose: True
[3] numpy.isclose の既定(rtol=1e-05, atol=1e-08)は緩め・非対称
np.isclose(1e-9, 0) = True np.isclose(100.0, 100.0009) = True
[4] ULP(隣の数との間隔)
math.ulp(1) = 2.220446049250313e-16 nextafter = 1.0000000000000002
math.ulp(1000) = 1.1368683772161603e-13 nextafter = 1000.0000000000001
math.ulp(1e+16) = 2.0 nextafter = 1.0000000000000002e+16
0.1+0.2 と 0.3 は 1 ULP 離れている
- 「差が EPSILON より小さいか」は、1 前後の数でしか使えません。 2000 付近では、0.1+0.2 と同じ「1 ULP 分」のずれでも差は 2.27 × 10のマイナス13乗 になり、EPSILON(2.22 × 10のマイナス16乗)を超えて false になりました。MDN の
Number.EPSILONのページも、2000 程度の数では小さすぎると書き、2000 * Number.EPSILONのように大きさに合わせる例を挙げています。 math.iscloseは既定で相対誤差 10のマイナス9乗 で比べます(Python のドキュメントでは「約9桁まで同じ」)。ただしabs_tolの既定は 0.0 なので、0 と比べると 0 以外はすべて False です。0 に近いかを見るときはabs_tolを足します。- NumPy の
np.iscloseは既定が rtol=10のマイナス5乗、atol=10のマイナス8乗 と緩く、100.0 と 100.0009 も等しいと判定しました。テストで使うときは、許容誤差を明示します。
各言語でのコピペ用の比較関数です。どれも「相対誤差か絶対誤差のどちらかに収まれば等しい」という math.isclose と同じ考え方です。
// JavaScript
function isClose(a, b, relTol = 1e-9, absTol = 0) {
return Math.abs(a - b) <= Math.max(relTol * Math.max(Math.abs(a), Math.abs(b)), absTol)
}
console.log(isClose(0.1 + 0.2, 0.3), isClose(1000.1 + 1000.2, 2000.3)) // true true
// Java
static boolean isClose(double a, double b, double relTol, double absTol) {
return Math.abs(a - b) <= Math.max(relTol * Math.max(Math.abs(a), Math.abs(b)), absTol);
}
/* C(math.h) */
int is_close(double a, double b, double rel_tol, double abs_tol) {
return fabs(a - b) <= fmax(rel_tol * fmax(fabs(a), fabs(b)), abs_tol);
}
JavaScript の関数は Node.js 22.22.2 で true true になることを確かめました。gcc の -Wfloat-equal を付けてコンパイルすると、浮動小数点数の == に次の警告が出ます。
sp.c:11:80: warning: comparing floating-point with '==' or '!=' is unsafe [-Wfloat-equal]
- 1計算した小数を == や != で比べていない0.1+0.2 と 0.3 は1 ULP ずれる。isclose などの関数で比べる
- 2許容誤差を数の大きさに合わせている固定の EPSILON は 1 前後でしか使えない。相対誤差を使う
- 30 と比べるときは絶対誤差も入れているmath.isclose(x, 0) は abs_tol が無いと常に False
- 4ループの回数を小数で数えていない回数は整数で数え、x = i / 10 のように計算する
- 5NaN を isnan で調べているNaN は x == NaN が常に false
- 6テストの期待値を許容誤差つきで書いているPython 3.12 の sum() のように、版で最後の桁が変わることがある
四捨五入がずれる理由(round・toFixed・Math.round)
この節では、言語ごとに四捨五入の結果が違う理由と、ずれない丸め方が分かります。
| 書き方 | 2.5 | −2.5 | 2.675 を小数2桁に |
|---|---|---|---|
| Python の round | 2 | −2 | 2.67 |
| JavaScript の Math.round・toFixed | 3 | −2 | 2.67 |
| Java の Math.round・String.format | 3 | −2 | 2.68 |
| C の round・printf("%.2f") | 3 | −3 | 2.67 |
| Python の Decimal(ROUND_HALF_UP) | ― | ― | 2.68 |
4言語で実行した結果です。ずれには2つの原因があります。
- 丸め方の規則が違う:Python の
roundは、ちょうど真ん中なら偶数に寄せる「偶数丸め(銀行丸め)」です(Python のドキュメントで、round(0.5)とround(-0.5)は 0、round(1.5)は 2)。JavaScript と Java のMath.roundは「+0.5 して切り捨て」なので −2.5 が −2、C のroundは「0 から遠い方」なので −3 になりました。 - 2.675 がそもそも 2.675 ではない:
Decimal(2.675)は 2.67499999999999982236431605997495353221893310546875 で、真ん中より少し小さいので 2.67 になります。Python のドキュメントも「round(2.675, 2)が 2.68 でなく 2.67 になるのはバグではない」と書いています。Java のString.format("%.2f", 2.675)は 2.68、String.format("%.2f", 1.005)は 1.01 になり、JavaScript の(1.005).toFixed(2)は 1.00 でした。
四捨五入の結果を正確にそろえたいときは、10進数のまま計算できる型(Python の Decimal、Java の BigDecimal)で、丸め方を指定します。 言語ごとの詳しい書き方は、関連記事「Python四捨五入はroundでずれる|切り捨て・切り上げ・Decimal」と関連記事「JavaScriptの小数計算誤差と丸め|toFixedの落とし穴と正しい対処法」で扱っています。
お金の計算で浮動小数点数を使わない方法(Decimal・整数)
この節では、金額や数量の計算で誤差を出さないための、型の選び方とコードが分かります。
まず、浮動小数点数のまま金額を計算すると何が起きるかです。
import math
from decimal import Decimal, ROUND_FLOOR, getcontext
print("[1] float で金額を計算したとき")
print(" 100 * 1.1 =", 100 * 1.1, " → 切り上げ math.ceil:", math.ceil(100 * 1.1))
print(" 0.29 * 100 =", 0.29 * 100, " → int():", int(0.29 * 100))
print(" 1.15 * 100 =", 1.15 * 100, " → int():", int(1.15 * 100))
total = 0.0
for _ in range(1000): total += 0.01
print(" 0.01 を1000回足す =", total)
print("[2] Decimal(文字列から作る)")
print(" Decimal('100') * Decimal('1.1') =", Decimal("100") * Decimal("1.1"), "→ 切り上げ", (Decimal("100") * Decimal("1.1")).to_integral_value(rounding="ROUND_CEILING"))
print(" Decimal('0.29') * 100 =", Decimal("0.29") * 100)
print(" Decimal(0.1) (float から作る) =", Decimal(0.1))
print(" Decimal('0.1') (文字列から作る) =", Decimal("0.1"))
t = sum(Decimal("0.01") for _ in range(1000)); print(" Decimal('0.01') を1000回足す =", t)
print(" getcontext().prec =", getcontext().prec)
print(" 税込(1円未満切り捨て) 1,980円×1.1:", (Decimal(1980) * Decimal("1.1")).quantize(Decimal("1"), rounding=ROUND_FLOOR))
print("[3] 整数(最小単位)で持つ")
price_sen = 29 # 0.29ドル = 29セント
print(" 29セント × 100個 =", price_sen * 100, "セント")
print(" 税込: 1980 * 110 // 100 =", 1980 * 110 // 100, " 100 * 110 // 100 =", 100 * 110 // 100)
[1] float で金額を計算したとき
100 * 1.1 = 110.00000000000001 → 切り上げ math.ceil: 111
0.29 * 100 = 28.999999999999996 → int(): 28
1.15 * 100 = 114.99999999999999 → int(): 114
0.01 を1000回足す = 9.999999999999831
[2] Decimal(文字列から作る)
Decimal('100') * Decimal('1.1') = 110.0 → 切り上げ 110
Decimal('0.29') * 100 = 29.00
Decimal(0.1) (float から作る) = 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1') (文字列から作る) = 0.1
Decimal('0.01') を1000回足す = 10.00
getcontext().prec = 28
税込(1円未満切り捨て) 1,980円×1.1: 2178
[3] 整数(最小単位)で持つ
29セント × 100個 = 2900 セント
税込: 1980 * 110 // 100 = 2178 100 * 110 // 100 = 110
100円の税込(10%、1円未満切り上げ)を float で計算すると、110.00000000000001 を切り上げて 111円になりました。 0.29 ドルを100倍してセントにすると 28 セントです。どちらも、切り上げ・切り捨ての直前に小さな誤差があると、1円・1セントの違いとして表に出ます。JavaScript(Node.js 22.22.2)でも Math.ceil(100*1.1) は 111、Math.floor(0.29*100) は 28 でした。
10進数の型(Decimal・BigDecimal・NUMERIC)
0.1 を 0.1 のまま持てる。丸め方も指定できる
税率・割引率・単価に小数がある計算に向く
文字列から作る。float から作ると誤差ごと入る
float より遅い(PostgreSQL のドキュメントも整数・浮動小数点数よりとても遅いと記載)
最小単位の整数(円・銭・セント)
金額を整数で持つので誤差が出ない
速く、どの言語・DB でも同じ結果になる
税率は 110 / 100 のように整数で掛けて割る
割り算の端数の扱いを自分で決めて書く
Java の BigDecimal でも同じことを確かめました。
import java.math.*;
public class Money {
public static void main(String[] a) {
System.out.println("100 * 1.1 = " + (100 * 1.1) + " Math.ceil = " + Math.ceil(100 * 1.1));
System.out.println("new BigDecimal(0.1) = " + new BigDecimal(0.1));
System.out.println("new BigDecimal(\"0.1\") = " + new BigDecimal("0.1"));
System.out.println("BigDecimal.valueOf(0.1) = " + BigDecimal.valueOf(0.1));
BigDecimal p = new BigDecimal("100").multiply(new BigDecimal("1.1"));
System.out.println("100 * 1.1 = " + p + " setScale(0, CEILING) = " + p.setScale(0, RoundingMode.CEILING));
System.out.println("equals: " + new BigDecimal("2.0").equals(new BigDecimal("2.00")) + " compareTo: " + new BigDecimal("2.0").compareTo(new BigDecimal("2.00")));
try { new BigDecimal("1").divide(new BigDecimal("3")); } catch (ArithmeticException e) { System.out.println(e); }
System.out.println("1 / 3 (scale 2, HALF_UP) = " + new BigDecimal("1").divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP));
}
}
100 * 1.1 = 110.00000000000001 Math.ceil = 111.0
new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
new BigDecimal("0.1") = 0.1
BigDecimal.valueOf(0.1) = 0.1
100 * 1.1 = 110.0 setScale(0, CEILING) = 110
equals: false compareTo: 0
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
1 / 3 (scale 2, HALF_UP) = 0.33
new BigDecimal(0.1)は、double の 0.1 の誤差をそのまま持ちます。 文字列のnew BigDecimal("0.1")か、BigDecimal.valueOf(0.1)(double を文字列にしてから作る)を使います。Python のDecimal(0.1)とDecimal("0.1")も同じ関係です。BigDecimalのequalsは桁数(scale)まで比べるので、2.0 と 2.00 は false です。値だけ比べるときはcompareToを使います。- 1 ÷ 3 のように割り切れない割り算は、桁数と丸め方を指定しないと
ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.で止まります。
BigDecimal の丸めと比較の詳しい書き方は、関連記事「Java BigDecimalの使い方|金額計算の丸めと比較の注意点」で扱っています。データベースでは、PostgreSQL のドキュメントが金額のように正確さが要る値に numeric 型を勧め、real・double precision は「不正確で可変精度」の型で、等しいかの比較が期待どおりにならないことがあると書いています。
固定小数点数との違い
この節では、固定小数点数と浮動小数点数の違いを、数の間隔(精度)で比べて分かります。
固定小数点数は、小数点の位置を決めておき、整数として保存する方法です。たとえば「65536 で割った値」と決めておけば(Q16.16 という形式)、整数 6554 が 0.100006… を表します。
[6] 数の間隔(ULP):値の大きさごと
1: fp64 2.22e-16 fp32 1.19e-07 fp16 0.000977 固定小数点Q16.16 1.53e-05
1000: fp64 1.14e-13 fp32 6.1e-05 fp16 0.5 固定小数点Q16.16 1.53e-05
1e+06: fp64 1.16e-10 fp32 0.0625 fp16 nan 固定小数点Q16.16 1.53e-05
1.67772e+07: fp64 3.73e-09 fp32 2 fp16 nan 固定小数点Q16.16 1.53e-05
[7] 固定小数点 Q16.16(整数を 65536 で割った値)
0.1 → 6554 → 0.100006103515625 0.1+0.2 → 0.3000030517578125
表せる最大 ≒ 32767.99998474121 最小の刻み 1.52587890625e-05
NumPy の np.spacing で、値の大きさごとの「隣の数との間隔」を出しました(fp16 の nan は範囲外)。浮動小数点数は、値が大きくなるほど間隔が広がります。 単精度では、100万付近で 0.0625 刻み、1677万付近で 2 刻みになり、小数点以下が表せなくなります。固定小数点数 Q16.16 はどこでも 1.53 × 10のマイナス5乗 刻みですが、表せるのは約 ±32768 までです。
| 項目 | 固定小数点数 | 浮動小数点数 |
|---|---|---|
| 小数点の位置 | 決まっている | 指数で動く |
| 刻み(精度) | どの大きさでも同じ | 大きい数ほど粗い |
| 表せる範囲 | 狭い(Q16.16 で約 ±32768) | 広い(倍精度で約 10の308乗) |
| 計算の仕組み | 整数の計算と同じ | 専用の回路(FPU)か命令 |
| 使われる場所 | 金額(10進の固定小数点)・FPU の無いマイコン・音声 | 科学技術計算・3DCG・AI・一般のプログラム |
データベースの DECIMAL(10, 2)・NUMERIC や、Python の Decimal・Java の BigDecimal は、10進数で桁を管理する型で、お金の計算に向いています(前の節)。
float と double どちらを使うか(型の選び方と言語別の型)
この節では、用途ごとにどの型を選ぶかと、各言語の型の名前の対応が分かります。
flowchart TD
Q{お金?} -->|はい| D[Decimal<br>整数]
Q -->|いいえ| A{AI?}
A -->|はい| B[bf16<br>fp8]
A -->|いいえ| F[double<br>大量なら float]
| 用途 | 選ぶ型 | 理由 |
|---|---|---|
| 金額・ポイント・在庫数 | Decimal・BigDecimal・整数 | 1円の誤差も許されない。丸め方を指定できる |
| 一般の計算・統計・座標 | 倍精度(double) | 約16桁あり、累積の誤差が目立ちにくい |
| 3DCG・ゲーム・大量の測定値 | 単精度(float) | メモリが半分。GPU が速い。約7桁で足りる場面 |
| 時刻・ID などの大きな整数 | 64ビット整数 | 倍精度は 2の53乗 を超えると整数がずれる |
| AI の学習・推論 | bf16・fp16・fp8 | メモリと計算量が減る。精度の不足は工夫で補う |
迷ったら倍精度(double)を選びます。 単精度は 0.1 を100万回足すと 958 ずれ(前の節)、1677万を超えると整数も正確に表せません。単精度を選ぶのは、メモリや速さの理由がはっきりしているときです。
| 言語 | 単精度 | 倍精度 | 10進数で正確に計算する型 |
|---|---|---|---|
| C・C++ | float | double | 標準には無い(ライブラリを使う) |
| Java | float | double | java.math.BigDecimal |
| Python | (NumPy の float32) | float | decimal.Decimal |
| JavaScript | (Float32Array の要素) | Number | 無い(整数なら BigInt) |
| C# | float | double | decimal |
| Go | float32 | float64 | 無い(math/big の Rat・Float) |
| PostgreSQL | real | double precision | numeric(decimal) |
Python の float は倍精度で(Python のドキュメントでは、ほぼすべての環境で IEEE 754 binary64)、単精度の型はありません。JavaScript の Number も倍精度だけです。Java では、Java 17 で JEP 306(Restore Always-Strict Floating-Point Semantics)が入り、浮動小数点数の計算が常に strictfp と同じ厳密な規則になりました。
機械学習の浮動小数点数(fp16・bf16・fp8)
この節では、AI の学習・推論で使う16ビット・8ビットの浮動小数点数の違いと、それぞれの弱点が実測で分かります。
AI のモデルは数十億個の数(パラメーター)を持つので、1つあたりのビット数を減らすと、メモリと計算時間が大きく減ります。
「70億 × 1パラメーターのバイト数」で計算した値です。GPU のメモリ(VRAM)がどれだけ要るかの計算は、関連記事「VRAMとは|確認方法・必要量の目安とLLMの計算式をPythonで解説」で扱っています。
| 形式 | 符号・指数・仮数 | 強み | 弱み |
|---|---|---|---|
| fp16(半精度) | 1・5・10 | bf16 より細かい(約3桁) | 最大 65504。小さい勾配が 0 になる |
| bf16(bfloat16) | 1・8・7 | 単精度と同じ範囲 | 精度が約2桁。1 + 0.001 が 1 になる |
| fp8 E4M3 | 1・4・3 | fp8 の中では細かい | 最大 448。無限大が無い |
| fp8 E5M2 | 1・5・2 | 範囲が広い(最大 57344) | 精度がさらに低い |
bfloat16 は、単精度(32ビット)の上位16ビットだけを残した形式です。 Intel のドキュメントは「FP32 の短縮バージョン」、Google Cloud の TPU のドキュメントは「bfloat16 と float32 のダイナミックレンジは同等」と説明しています。実際に、単精度の 0.1(0x3DCCCCCD)は bf16 で 0x3DCD、1.0(0x3F800000)は 0x3F80 になり、上位16ビットと一致しました(0.1 は下の桁を丸めて切り上がっています)。
ml_dtypes 0.6.0 で、学習中に出てくる大きい値・小さい値を各形式に入れました。
import numpy as np, ml_dtypes as md
B = md.bfloat16; H = np.float16; E4 = md.float8_e4m3fn; E5 = md.float8_e5m2
def c(v, t): return float(np.array(v, dtype=np.float32).astype(t).astype(np.float32))
print("[1] 大きい値(活性化・ロジット)")
for v in (1000.0, 65504.0, 70000.0, 1e10):
print(f" {v:>10g}: fp16 {c(v,H)!r:<9} bf16 {c(v,B)!r:<14} e4m3 {c(v,E4)!r:<6} e5m2 {c(v,E5)!r}")
print("[2] 小さい値(勾配)")
for v in (1e-4, 1e-6, 1e-8, 1e-10):
print(f" {v:>10g}: fp16 {c(v,H)!r:<24} bf16 {c(v,B)!r:<24} e4m3 {c(v,E4)!r:<6} e5m2 {c(v,E5)!r}")
print("[3] 1 に小さい数を足す(精度)")
for d in (0.001, 0.01):
print(f" 1 + {d}: fp32 {float(np.float32(1)+np.float32(d))!r} fp16 {float(H(1)+H(d))!r} bf16 {float(np.array(1,B)+np.array(d,B))!r}")
print("[4] ロス・スケーリング(勾配 1e-8 を 65536 倍して fp16 に入れ、fp32 に戻して割る)")
g = 1e-8; s = 65536.0
print(f" そのまま fp16: {c(g,H)!r} スケール後 fp16: {c(g*s,H)!r} → 戻す {c(g*s,H)/s!r}")
[1] 大きい値(活性化・ロジット)
1000: fp16 1000.0 bf16 1000.0 e4m3 nan e5m2 1024.0
65504: fp16 65504.0 bf16 65536.0 e4m3 nan e5m2 inf
70000: fp16 inf bf16 70144.0 e4m3 nan e5m2 inf
1e+10: fp16 inf bf16 9999220736.0 e4m3 nan e5m2 inf
[2] 小さい値(勾配)
0.0001: fp16 0.00010001659393310547 bf16 0.00010013580322265625 e4m3 0.0 e5m2 0.0001068115234375
1e-06: fp16 1.0132789611816406e-06 bf16 9.98377799987793e-07 e4m3 0.0 e5m2 0.0
1e-08: fp16 0.0 bf16 1.0011717677116394e-08 e4m3 0.0 e5m2 0.0
1e-10: fp16 0.0 bf16 1.000444171950221e-10 e4m3 0.0 e5m2 0.0
[3] 1 に小さい数を足す(精度)
1 + 0.001: fp32 1.0010000467300415 fp16 1.0009765625 bf16 1.0
1 + 0.01: fp32 1.0099999904632568 fp16 1.009765625 bf16 1.0078125
[4] ロス・スケーリング(勾配 1e-8 を 65536 倍して fp16 に入れ、fp32 に戻して割る)
そのまま fp16: 0.0 スケール後 fp16: 0.0006551742553710938 → 戻す 9.997165761888027e-09
(fp16 の 70000 の変換では、NumPy が RuntimeWarning: overflow encountered in cast を出しました。)
- fp16 は 70000 で inf になり、10のマイナス8乗 の勾配は 0 になりました。 NVIDIA の混合精度学習のドキュメントは、FP16 の最大は 65504、最小の非正規化数は約 5.96 × 10のマイナス8乗 で、ある検出モデルでは勾配の 31% が FP16 で 0 になったと書いています。対策がロス・スケーリングで、損失に大きな数を掛けてから逆伝播し、後で割り戻します。[4] では、65536 倍して fp16 に入れてから戻すと、10のマイナス8乗 がほぼ残りました。
- bf16 は範囲が単精度と同じなので、70000 も 10のマイナス10乗 も入ります。 代わりに仮数部が7ビットしかなく、1 + 0.001 は 1 のままでした。学習では、重みのコピーを単精度で持ち、足し込みは単精度で行う使い方が一般的です。
- fp8 は、範囲と精度のどちらも足りません。 NVIDIA の Transformer Engine のドキュメントによると、E4M3 は最大 ±448 と NaN(無限大なし)、E5M2 は最大 ±57344・無限大・NaN を持ちます。順伝播の活性化と重みには精度の高い E4M3、逆伝播の勾配には範囲の広い E5M2 を使い、テンソルごとに別のスケーリング係数を掛けて範囲に収めます。ml_dtypes の変換では、E4M3(fn は finite の略で無限大を持たない)の範囲を超えた値は NaN になりました。
さらに小さい4ビットの浮動小数点数(FP4 の E2M1。符号1・指数2・仮数1)も、ml_dtypes で全部の値を出すと、正の値は 0・0.5・1・1.5・2・3・4・6 の8つだけでした。これだけでは足りないので、ブロックごとに倍率を持たせて使います。GPU で LLM を動かすときの量子化の大きさは、関連記事「VRAMとは|確認方法・必要量の目安とLLMの計算式をPythonで解説」で計算しています。
浮動小数点数のよくあるエラーと間違い
この節では、浮動小数点数で出るエラー・警告の原文と、その直し方が分かります。原文はすべて手元で実行して出たものです。
| エラー・警告の原文 | 原因 | 直し方 |
|---|---|---|
ZeroDivisionError: float division by zero |
Python で float を 0 で割った | 割る前に 0 か調べる。inf がほしいなら NumPy |
OverflowError: math range error |
math.exp(1000) など結果が最大値を超えた | 対数のまま計算する(logsumexp など) |
OverflowError: int too large to convert to float |
10の400乗 の整数を float にした | 整数か Decimal のまま計算する |
ValueError: math domain error |
math.sqrt(-1)・math.log(0) | 引数の範囲を調べる。複素数は cmath |
ValueError: cannot convert float NaN to integer |
NaN を int にした | 先に math.isnan で除く |
OverflowError: cannot convert float infinity to integer |
inf を int にした | 先に math.isinf で除く |
RuntimeWarning: invalid value encountered in scalar divide |
NumPy で 0 ÷ 0(結果は nan) | np.errstate で扱いを決める・入力を確かめる |
RuntimeWarning: overflow encountered in cast |
NumPy で fp16 などの範囲外に変換 | 変換前に範囲を確かめる・bf16 を使う |
OverflowError: float too large to pack with e format |
struct で半精度の範囲外を詰めた | 値を 65504 以下に収める |
ArithmeticException: Non-terminating decimal expansion; … |
BigDecimal で割り切れない割り算 | divide に桁数と RoundingMode を渡す |
NumberFormatException: For input string: "1,5" |
Java で桁区切りの文字列を parseDouble | カンマを除くか NumberFormat で読む |
warning: comparing floating-point with '==' or '!=' is unsafe |
gcc の -Wfloat-equal | 許容誤差で比べる |
エラーにならずに値がおかしくなる間違いもあります。
1表示の桁で判断する
C の %g や toFixed で 0.3 と表示されても、中身は 0.30000000000000004。調べるときは17桁か Decimal で表示する。
2float から Decimal を作る
Decimal(0.1)・new BigDecimal(0.1) は誤差ごと入る。文字列から作るか valueOf を使う。
3大きな ID を JSON の数値で送る
JavaScript では 9007199254740993 が 9007199254740992 になる。ID は文字列で送る。
4単精度で大きな合計を出す
0.1 を100万回足して 958 ずれた。合計は倍精度か fsum・NumPy の sum で計算する。
よくある質問
Q. 浮動小数点数とは簡単に言うと何ですか? A. 数を「符号・指数・仮数」の3つに分けて、「仮数 × 2の指数乗」の形で表す方法です。少ないビット数でとても大きな数から小さな数まで表せる代わりに、多くの小数は近似値になります。
Q. 浮動小数点の読み方は? A. 「ふどうしょうすうてん」と読みます。英語は floating point(フローティングポイント)で、数は floating-point number です。
Q. float と double の違いは? A. float は単精度(32ビット、10進数で約7桁)、double は倍精度(64ビット、約16桁)です。double のほうが範囲も精度も大きく、迷ったら double を選びます。Python の float と JavaScript の Number は double と同じ倍精度です。
Q. 単精度と倍精度の有効桁数は? A. 単精度は約6〜9桁(マシンイプシロン 1.19 × 10のマイナス7乗)、倍精度は約15〜17桁(2.22 × 10のマイナス16乗)です。半精度は約3〜4桁、bfloat16 は約2〜3桁です。
Q. なぜ 0.1 + 0.2 は 0.3 にならないのですか? A. 0.1 と 0.2 は2進数で割り切れず、近い値に丸めて保存されるためです。足した結果がもう一度丸められ、0.3 を直接入れた値と隣り合った別の数(差 5.55 × 10のマイナス17乗)になります。Python・JavaScript・Java・C のどれでも同じ結果でした。
Q. 浮動小数点数の誤差をなくすことはできますか? A. 浮動小数点数のままでは、なくせません。比較は許容誤差で行い、お金のように誤差が許されない計算は Decimal・BigDecimal か整数を使います。合計は math.fsum のような誤差を抑える関数で小さくできます。
Q. ケチ表現とは何ですか? A. 正規化した仮数は必ず「1.xxx」になるので、先頭の 1 を保存しない表し方です。単精度は仮数部23ビットで24ビット分、倍精度は52ビットで53ビット分の精度を持てます。
Q. 浮動小数点数の変換問題(基本情報)はどう解きますか? A. 10進数を2進数にし、1.xxx × 2のn乗 に正規化して、符号・指数(n にバイアスを足す)・仮数(先頭の 1 を除く)を並べます。5.75 なら 0 10000001 0111000… になります(手計算の節)。
Q. 固定小数点数との違いは? A. 固定小数点数は小数点の位置が決まっていて刻みがどこでも同じ、浮動小数点数は小数点が動き、範囲が広い代わりに大きな数ほど刻みが粗くなります。単精度は100万付近で 0.0625 刻みでした。
Q. bf16 と fp16 の違いは? A. どちらも16ビットですが、bf16 は指数8ビット・仮数7ビットで単精度と同じ範囲、fp16 は指数5ビット・仮数10ビットで精度が高い代わりに最大 65504 です。fp16 で学習するときは、勾配が 0 にならないようロス・スケーリングを使います。
Q. NaN とは何ですか? A. Not a Number(非数)の略で、0 ÷ 0 や ∞ − ∞ のように答えが決まらない計算の結果です。自分自身とも等しくないので、判定は math.isnan・Number.isNaN を使います。
まとめ
- 浮動小数点数は、数を符号・指数・仮数に分け、「仮数 × 2の指数乗」で表す。IEEE 754 の倍精度は 1・11・52 ビット、単精度は 1・8・23 ビット。
- 0.1 は分母に 5 を含むので2進数で割り切れず、近い値に丸めて保存される。0.1+0.2 は 0.3 と1 ULP ずれ、4言語とも 0.30000000000000004 になった。
- 誤差には丸め誤差・情報落ち・桁落ち・オーバーフロー/アンダーフローがあり、足す順番でも結果が変わる。単精度で 0.1 を100万回足すと 958 ずれた。
- 比較は == でなく、大きさに合わせた許容誤差(math.isclose など)で行う。0 と比べるときは絶対誤差も入れる。
- お金は Decimal・BigDecimal(文字列から作る)か、最小単位の整数で計算する。float のままだと 100円の税込が 111円になった。
- AI では bf16・fp16・fp8 が使われ、範囲と精度の弱点をロス・スケーリングやスケーリング係数で補う。
参考・出典
確認日はいずれも 2026年10月22日です。
- IEEE「IEEE 754-2019 - IEEE Standard for Floating-Point Arithmetic」:https://standards.ieee.org/ieee/754/6210/
- Python ドキュメント「15. 浮動小数点演算、その問題と制限」:https://docs.python.org/ja/3/tutorial/floatingpoint.html
- Python ドキュメント「math — Mathematical functions」(isclose・fsum・ulp・nextafter):https://docs.python.org/3/library/math.html
- Python ドキュメント「Built-in Functions」(round):https://docs.python.org/3/library/functions.html
- Python ドキュメント「What's New In Python 3.12」(sum の Neumaier 加算):https://docs.python.org/3/whatsnew/3.12.html
- MDN「Number.EPSILON」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON
- MDN「Number.MAX_SAFE_INTEGER」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
- Oracle「Float(Java SE 21 API)」(floatToFloat16):https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Float.html
- OpenJDK「JEP 306: Restore Always-Strict Floating-Point Semantics」:https://openjdk.org/jeps/306
- OpenJDK「JDK 17」:https://openjdk.org/projects/jdk/17/
- PostgreSQL ドキュメント「8.1. Numeric Types」:https://www.postgresql.org/docs/current/datatype-numeric.html
- NVIDIA「Train With Mixed Precision」:https://docs.nvidia.com/deeplearning/performance/mixed-precision-training/index.html
- NVIDIA Transformer Engine「Using FP8 with Transformer Engine」:https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html
- Google Cloud「Improve your model's performance with bfloat16」:https://docs.cloud.google.com/tpu/docs/bfloat16
- Intel「bfloat16 ハードウェア数値定義」:https://intel.co.jp/content/www/jp/ja/content-details/671279/bfloat16-hardware-numerics-definition.html
- ml_dtypes(PyPI):https://pypi.org/project/ml-dtypes/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「浮動小数点数とは|仕組み・誤差・0.1+0.2の理由を4言語で実演」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。