floatの構造
IEEE 754の代表的な浮動小数点形式は、次の3つの部分に分けられる。
wiki浮動小数点数
- Sign:符号
- Exponent:指数
- Fraction:仮数部の小数部分
例えば、float32の場合、
| Sign | Exponent | Fraction |
| 1 | 8 | 23 |
合計32 bitとなる。
通常の正規化された2進浮動小数点数では、次の式で数値を表現する。(AI先生より)
$$
(-1)^S \times (1+M) \times 2^{E-Bias}
$$
-
S:符号ビット(0:正、1:負) -
E:指数部を符号なし整数として解釈した値 -
M:仮数部を2進小数として解釈した値(先頭の暗黙の1は含まない) -
Bias:指数バイアス
※この式は正規化数(Normal Number)の場合に使用する。
※川口の数学力では、この式を見ただけではさっぱり理解できなかった。なので、実際に数字を代入しながら確認してみる。
主な形式を比較すると、以下のようになる。
| データ型 | Sign | Exponent | Fraction | 合計 |
|---|---|---|---|---|
| float32 | 1 | 8 | 23 | 32 |
| float16 | 1 | 5 | 10 | 16 |
| bfloat16 | 1 | 8 | 7 | 16 |
| float8_e4m3fn | 1 | 4 | 3 | 8 |
| float8_e5m2 | 1 | 5 | 2 | 8 |
詳しく見てみよう。
実験:同じ数値をfloatに変換してみる
まずはPyTorchで、同じ数値を異なるデータ型に変換してみる。
import torch
values = [1000.0, 1.1, 0.001]
dtypes = [
torch.float32,
torch.float16,
torch.bfloat16,
torch.float8_e4m3fn,
torch.float8_e5m2,
]
for value in values:
print(f"\n元の値: {value}")
for dtype in dtypes:
tensor = torch.tensor(
[value],
dtype=torch.float32
).to(dtype)
result = tensor.to(torch.float32).item()
print(dtype, result)
float32を経由して変換しているのは、float8への変換を試すため。
実行結果
以前の実験では、次の結果が得られた。
| データ型 | 1000.0 | 1.1 | 0.001 |
|---|---|---|---|
| float32 | 1000.0 | 1.100000023841858 | 0.0010000000474974513 |
| float16 | 1000.0 | 1.099609375 | 0.0010004043579101562 |
| bfloat16 | 1000.0 | 1.1015625 | 0.00099945068359375 |
| float8_e4m3fn | 448.0 | 1.125 | 0.001953125 |
| float8_e5m2 | 1024.0 | 1.0 | 0.0009765625 |
想像以上に誤差が大きい。
特に気になったのは、
1000.0
↓
448.0
という結果だ。
1000という数値が448になってしまった。
これでは、単なる丸め誤差とは言えないのではないか?
なぜ1000が448になるのか?
float8_e4m3fnは、符号に1 bit、指数に4 bit、仮数部に3 bitを使用する。
では、表現できる最大の有限値を計算してみよう。
- 符号部に1 bit:
0 - 指数部に4 bit:
1111 - 仮数部に3 bit:
110
仮数部は合計3 bitなので、普通に考えれば、3 bitすべてが1の
111が最大になるはずだ。しかし、実際にはそうではない。
float8_e4m3fnでは、符号部が0か1かにかかわらず、指数部と仮数部が両方ともすべて1の場合、非数(NaN)を表す。ここで注意したいのは、NaNの表現方法はfloatの形式によって異なるということだ。
float8_e4m3fnの最大有限値は、符号部:0、指数部:1111、仮数部:110となる。一方、
float8_e5m2では、指数部が11111で仮数部が00の場合はInfinity、それ以外の場合はNaNとなる。そのため、最大有限値は符号部:0、指数部:11110、仮数部:11となる。
S = 0
(-1)^Sなので、符号部が1なら負、0なら正となる。
なぜ(-1)^1=-1、(-1)^0=1になるのかは、川口には説明できないので、ぜひ電卓やExcelで確認していただきたい。
E = 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0 = 15
指数部が4 bitで、1111の場合。
2^4-1でもOK。
| bit | 4bit | 3bit | 2bit | 1bit |
|---|---|---|---|---|
| 二進法 | 1 | 1 | 1 | 1 |
| 十進法への変換(2^x) | 1*2^3 | 1*2^2 | 1*2^1 | 1*2^0 |
| 十進法 | 8 | 4 | 2 | 1 |
Bias = 2^3-1 = 7
指数部は4 bitなので、2^(4-1)-1となる。
1 + M = 1 + (1*2^2 + 1*2^1 + 0*2^0) / 8 = 1.75
仮数部は合計3 bitなので、2^3 = 8となる。
| bit | 3bit | 2bit | 1bit |
|---|---|---|---|
| 二進法 | 1 | 1 | 0 |
| 十進法への変換(2^x) | 1*2^2 | 1*2^1 | 0*2^0 |
| 十進法 | 4 | 2 | 0 |
計算式に代入すると、
1 * 1.75 * 2^(15-7) = 448
となる。
つまり、float8_e4m3fnが表現できる有限値の範囲は[-448, 448]となる。
float8_e4m3fnでは、そもそも1000を表現できないため、今回の実験では、表現可能な最大有限値である448になった。
今回の実験環境では、
1000.0をfloat8_e4m3fnに変換した結果、表現可能な最大有限値である448.0になった。ただし、範囲外の数値が必ず最大値になるわけではなく、変換時のオーバーフロー処理によって結果が異なる場合がある。(AI先生より)
一方、float8_e5m2では指数部が5 bitになる。その分、合計8 bitに収めるため、仮数部は2 bitになる。
最大有限値を表すbitは、以下のとおり。
- 符号部に1 bit:
0 - 指数部に5 bit:
11110 - 仮数部に2 bit:
11
計算すると、
S = 0E = 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 0*2^0 = 30Bias = 2^4-1 = 151 + M = 1 + (1*2^1 + 1*2^0) / 4 = 1.75
1 * 1.75 * 2^(30-15) = 57344
となる。
つまり、float8_e5m2が表現できる有限値の範囲は[-57344, 57344]となる。
あれ?1000は[-57344, 57344]の範囲内に含まれているのに、なぜ1024になったのだろうか?
AI先生にfloat8_e5m2の1024のbit表現を聞いてみたところ、答えは01100100だった。
では、このbit列を符号なし整数として扱い、1を引いてみよう。
01100011になる。
やりたくないが、計算してみようか。
S = 0E = 1*2^4 + 1*2^3 + 0*2^2 + 0*2^1 + 0*2^0 = 24Bias = 2^4-1 = 151 + M = 1 + (1*2^1 + 1*2^0) / 4 = 1.751 * 1.75 * 2^(24-15) = 896
結果は896だった。
つまり、float8_e5m2では、896の次に表現できる数値は1024となり、その間にある数値を正確に表現できないということだ。
では、なぜ1000は896ではなく、1024になったのだろうか?
1000との距離を計算してみる。
1000 - 896 = 104
1024 - 1000 = 24
なるほど、1024の方が1000に近い。
つまり、今回の変換では、1000は表現可能な数値のうち、より近い1024に丸められたということだ。
float8_e5m2は表現できる数値の範囲が広いが、その範囲内のすべての数値を正確に表現できるわけではないことが分かった。
何だか、次のような結論が見えてきた気がしないか?
- 合計bit数が同じ場合、指数部に割り当てるbit数を増やすと、表現できる数値の範囲が広くなる。
- 一方、仮数部のbit数が減ると、同じ指数の範囲内で細かく表現できる数値が少なくなる。
おお、すごいじゃん。float型の範囲と精度の関係が見えてきたじゃないか。
つまり、
- 指数部:範囲(どのくらい大きい、または小さい数値まで表現できるか)
- 仮数部:精度(同じくらいの大きさの数値を、どのくらい細かく区別できるか)
ということになる。