4 ビット Float を自作したら、浮動小数点数の精度を理解できた


はじめに
===============

浮動小数点数、**なんとなくはわかっているものの実は理解がちょっと曖昧**、という方いませんか？　僕は恥ずかしながら長らくそんな状態でした。

特に誤差と精度についてはかなり曖昧で、 **「どれくらいの数値であればどれくらいの精度があるのか」** という点は全く自信がありませんでした[^32bit_error]。（業務上、特に困ったこともなかった）

この記事は、**試しに４ビットの浮動小数点数を作ってみることにより、浮動小数点数の精度を理解する**ことを目指します。
想定の読者さんは、 **「浮動小数点数に関してはざっくり分かっているけど、実は精度や誤差に関してはちゃんと理解していない」** という方々です。具体的に言えば、緯度経度を32ビット浮動小数点数で表すと誤差がどれくらいになるのかがあまりピンと来ない[^latlng]、という方などです[^float]。

[^32bit_error]: 32 ビットの float は、いくつくらいの値だとどれくらいの精度が出るのか（一般的な仕様であるの 32 ビット float では、ある値以上になると、最も近い数値同士の差が 1.0 を超えるのだけど、それはいくつくらい？）

[^latlng]: 賃貸物件の検索サービスを作っている中で、緯度経度を保持する場合に32ビットだとどれくらいの精度かを確認する必要が出てきました。さて、日本国内の緯度経度を 32 ビット浮動小数点数で表現する場合、最大の誤差は何メートルくらいになるでしょうか？

[^float]: 浮動小数点自体を初めて聞く方はあまり対象の読者として想定していないので、書籍や別の記事（例えば「[浮動小数点って何?](https://qiita.com/angel_p_57/items/24078ba4aa5881805ab2)」）等を参照された方がよいかもしれません。






浮動小数点数の概要
===================

ここでは、ざっくり浮動小数点数の概要に関して復習程度に触れます。
概要は分かっているという方は読み飛ばして頂いて問題ないです。

浮動小数点数とは
--------------

浮動小数点数は、**コンピュータで小数を扱うための数値の表現方法**です。小数を表す際には最も一般的に用いられています。

コンピュータで数値を表す際には**有限のビット数で表現する必要がある**ため、当然ながら表せる数値も有限の個数となります。そこで、 **値の範囲と値同士の刻み幅（差分）** を決める必要があります。

整数を表現する場合は、刻み幅は基本的に $1$ としておけば問題ないので、値の範囲のみを決めればすんなりいきそうです[^minus]。 8 ビットであれば 256 種類の値を表現できるので、「 $0$ から $255$」または「 $-128$ から $127$ 」としておけばいい感じです。

[^minus]: 負の値を補数として表現するといった点もありますが、いったんここでは触れません。
ですが、小数の場合は、いくつくらいにすればよいでしょうか？

では小数はどのように表現するのがよいでしょうか？　整数と同じように、範囲を決めた上で、値同士の差分を一定の値としてしまうのがシンプルです。例えば、 $0$ から $25.5$ の値を $0.1$ 区切りで表現する、といった方法です。このような方法は**固定小数点数**と呼ばれ、**精度が一定であることが要求される場面では現在でも実際に用いられています**。

これに対して浮動小数点数は、**小さい数字ほど刻み方を小さくして、大きくなればなるほど刻み方を大きくする**、という方式です。 $0$ に近いところでは、幅はとても小さくなり、大きい数字になると刻み幅 $1$ をも超えてしまいます。**実際 32 ビットの浮動小数点数では、 $16,777,216$ （1677万）あたりで刻み幅は $1$ を超えます。**

しかし、**小さい値は小さく刻み、大きい値はそこまでの刻み幅を必要としない**という考え方は、**多くの実践的な場合に即しており**、そのため浮動小数点数は最も一般的に用いられることとなっています。


自作 4 ビット浮動小数点数
====================

一般的な浮動小数点数の仕様
---------------

まずは浮動小数点数の最も一般的な仕様である IEEE754 形式を確認してみましょう。
浮動小数点数は、**符号部・指数部・仮数部** という ３ つの要素から構成されます[^digits]。

![浮動小数点数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/0c79718a-2f2c-4168-b8ee-fec3bea76cf2.png)

[^digits]: 図中の指数部・仮数部の桁数は分かりやすさのために短くしてあり、実在する浮動小数点数ではありません。IEEE754 形式では、単精度浮動小数点数（32bit Float）は、指数部が 8 ビットで仮数部 23 ビット、倍精度浮動小数点数（64bit Float または Double と呼ばれる）は、指数部が11 ビットで仮数部 52 ビットとなっています。

符号部は「正の値か負の値か」を表すためのもので、浮動小数点数の実際の値は下記で計算されるものとなります。

![浮動小数点数_equation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/7e42087c-79f5-e648-cd51-5d8aee22ffb5.png)

…なんか**ピンとこない**ですよね…（上の式でスッキリしている方は、これ以上読み進めてもなにも得られないかもしれません…）

試しに、すごく限定した仕様の浮動小数点数を作ってみて、具体的にどういった値になるのか実験してみましょう。


自作 4 bit Float の仕様
---------------

とてもシンプルな仕様の浮動小数点数として、**符号部がなく仮数部と指数部がそれぞれ 2 ビットだけ**しかない **計 4 ビットの浮動小数点数**（以下 **4bit Float** とします）を作ってみます[^neg_value]。

[^neg_value]: 符号部がないため負の数値は表せませんが、ここでは指数部と仮数部を変えた時に実際の値がどう変化していくのかを見てみましょう。

### 値の例

まずは具体的な 4bit Float の例として　$1001$　という表記が表す値を考えてみます。

![浮動小数点数_4bit_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/cc0b8af4-d656-141e-b41e-00f594be7643.png)


この表記の実際の値は下記のように計算されます。

![浮動小数点数_4bit_equation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/077fa961-288f-cb2f-9087-71049a780448.png)



仮数部の　$1.01$　と指数部の　$10$　は 2 進数表記であり、 $x$ が 2 進数表記である場合に $(x)_2$、 $y$ が 10 進数である場合にはそのまま $y$ と表記する形式を用いると


```math
\begin{align}
(1.01)_2 &= 1 \times 2^0 + 0 \times 2^{-1} + 1 \times 2^{-2} \\
         &= 1 \times 1 + 0 \times 0.5 + 1 \times 0.25  \\
         &= 1 + 0 + 0.25  \\
         &= 1.25 \\
(10)_2 &= 1 \times 2^1 + 0 \times 2^0 \\
       &= 2 \\
\end{align}
```

となり、10進数の実数値は、
```math
\begin{align}
(1.01)_2 \times 2^{(10)_2} &= 1.25 \times 2^{2} =  1.25 \times 4 \\
&= 5.0
\end{align}
```

となります。

### 全種類の値を確認してみる

この 4bit Float は 4 ビットのみで数値を表現するため、**全部で　$2^{4} = 16$　種類の数値しか表せません**。いっそ **16 種類の値を全部確認**してみましょう。

ますは、**指数部が $00$ となる 4 つの数値**を見てみます。指数部が $00$ ということは、仮数部にかける値が　$2^{(00)_2} = 2^{0} = 1$　となるため、**実際の値は仮数部の値そのもの**となります。

```math
\begin{align}
(0000)_2 &= (1.00)_2 \times 1 = 1.00 \times 1 = 1.00 \\
(0001)_2 &= (1.01)_2 \times 1 = 1.25 \times 1 = 1.25 \\
(0010)_2 &= (1.10)_2 \times 1 = 1.50 \times 1 = 1.50 \\
(0011)_2 &= (1.11)_2 \times 1 = 1.75 \times 1 = 1.75 \\
\end{align}
```

**最小値が $1.00$ 、最大値が $1.75$ で $0.25$ 刻み**になりました。

次に、**指数部が $01$ となる値**を見てみます。指数部が $01$ ということは実際の値は仮数部の値に　$2^{(01)_2} = 2^{1} = 2$　をかけた値となります。

```math
\begin{align}
(0100)_2 &= (1.00)_2 \times 2 = 1.00 \times 2 = 2.0 \\
(0101)_2 &= (1.01)_2 \times 2 = 1.25 \times 2 = 2.5 \\
(0110)_2 &= (1.10)_2 \times 2 = 1.50 \times 2 = 3.0 \\
(0111)_2 &= (1.11)_2 \times 2 = 1.75 \times 2 = 3.5 \\
\end{align}
```

今度は、**最小値が $2.0$、 最大値が $3.5$ で $0.5$ 刻み**になりました。

次は、**指数部が $10$ となる値**です。実際の値は**仮数部の値に 　$2^{(10)_2} = 2^{2} = 4$　 をかけた値**となります。

```math
\begin{align}
(1000)_2 &= (1.00)_2 \times 4 = 1.00 \times 2 = 4.0 \\
(1001)_2 &= (1.01)_2 \times 4 = 1.25 \times 2 = 5.0 \\
(1010)_2 &= (1.10)_2 \times 4 = 1.50 \times 2 = 6.0 \\
(1011)_2 &= (1.11)_2 \times 4 = 1.75 \times 2 = 7.0 \\
\end{align}
```

**最小値が $4.0$、 最大値が $7.0$ で $1.0$ 刻み**です。

最後は、**指数部が $11$ となる値**です。実際の値は**仮数部の値に $2^{(11)_2} = 2^{3} = 8$ をかけた値**となります。

```math
\begin{align}
(1000)_2 &= (1.00)_2 \times 8 = 1.00 \times 8 = 8.0 \\
(1001)_2 &= (1.01)_2 \times 8 = 1.25 \times 8 = 10.0 \\
(1010)_2 &= (1.10)_2 \times 8 = 1.50 \times 8 = 12.0 \\
(1011)_2 &= (1.11)_2 \times 8 = 1.75 \times 8 = 14.0 \\
\end{align}
```

**最小値が $8.0$、 最大値が $14.0$ で、刻み幅は 1 を超えて $2.0$ 刻み**になりました。


上記を表にまとめます。
![浮動小数点数_table_nobias_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/f2d59c2b-4f23-b0f7-8b0c-5ce848f53ace.png)

これらから、**4bit Float の下記の特徴**が分かります。

- **$1.0$ から $14.0$ までの数値を表現可能**
- 2進数表記の値が増えるにしたがって、**実数値も増加する（単調増加の関係になっている）**
- **数値の間隔は指数値の値のみによって決まる**
    - 指数部が最小の $(00)_2$ だと、間隔は $0.25$
    - 指数部が最大の $(11)_2$ だと、間隔は $2.0$ となる

### 指数部のゲタ履き表現

最小値が 1.0 だと、なんだか**浮動小数点数として弱い感じがするので、もっと小さい値を表現したくなります**。仮数部は $1.00, 1.25, 1.50, 1.75$ の 4 種類に固定したまま、指数部をもっと小さくしてみましょう。指数部の各値を -1 してみると、指数部は ${-1}, 0, 1, 2$ の 4 種類となり、その際の取りうる値は下記の表のようになります。

![浮動小数点数_taboe_withbias.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138646/6cf35580-77b3-f5fc-1bf2-50cc3a94efee.png)

最小値は $0.50$ で次の値は $0.125$ 増えた $0.625$ になり、**ちょっと浮動小数点数っぽさが増えました**。この「指数部の値をちょっとずらす表現」は、**ゲタ履き表現、バイアス表現、オフセット・バイナリ**などと呼ばれ、実際の浮動小数点数の指数部にも用いられています[^biased_repr]。

[^biased_repr]: IEEE754 形式の 32bit の単精度浮動小数点数では、指数部が 8 ビットであり、バイアスの値は $127$ であるため、仮数部にかける値の取りうる範囲は $-126$ から $127$ （ご指摘を頂き初稿から修正した内容です。-127 と 128 は特殊な扱いとなるため、-126 から 127 が正しい範囲となります。失礼いたしました。）となります。

### 浮動小数点数の精度

上記の 4bit Float から、だんだんと**浮動小数点数の表現可能な精度**が見えてきます。

**仮数部の種類の数は、仮数部のビット数に依存します**。4bit Float の場合は、2ビットだったために仮数部は $2^2 = 4$ 種類でした。IEEE754 形式の単精度浮動小数点数では、仮数部は 23 ビットのため、仮数部の取りうる値は $2^{23} = 8,388,608$ （約 840 万）種類となります。

そして、浮動小数点数の精度は **（仮数部の種類の数 / $2^{指数部}$ の値)** となります。 4bit Float の最初の例では、指数部が $0$ の場合は $1.0$ から $2.0$ の間に仮数部の種類の数 $4$ 種類の実数が存在することとなり、その間隔は ${1.0} \div {4} = 0.25$ となりました。$2.0$ から $4.0$ の間の数値の幅は $2.0 \div 4 = 0.5$ となります。
IEEE754 形式の単精度浮動小数点数では、（バイアスなしとして考えた場合の）指数部が $0$ の場合、つまり**実際の値が $1.0$ から $2.0$ となる間には、仮数部の種類数 $8,388,608$ の数値が存在**することになります。同様に $2.0$ から $4.0$ の間にも約 840 万種類、$4.0$ から $8.0$ の間にも約 840 万種類の数値が割り当てられます。
そして、指数部が $2^{23}$ となるとき、すなわち $8,388,608$ から $16,777,215$ となる時についに数値の幅は $1.0$ となります。**$16,777,215$ を超えるとついに数値の間隔が $2.0$ となるため、$16,777,217$ などは「表現できない数値」** ということになります。


最後に
======

本記事では、他にも色々と触れたい内容があったのですが長くなりすぎてしまうため、ここまでの内容といたします。例えば、仮数部はなぜ ${(1.xxx)_2}$ と最初に $1$ が付くのか、これが ${(0.xxx)_2}$ だったらどういう問題があるかといった点や、緯度経度の表現を 64bit の Double ではなく 32bit の Float だとどれくらいの距離の精度まで表現できるのか、などの問題もとても面白いです。（これらの問題は自分で考えてみても楽しめるかもしれません）

もし本記事が好評であれば、それらも続編の記事として書きたいと思いますので、**ご興味があればいいねを頂けると嬉しいです**（続編には興味がなくてもいいねを頂けると嬉しいです…！！！）。

ここまでお読み頂きありがとうございました。
[Twitter もやってますので](https://twitter.com/choo_s)、もしよろしければフォロー頂けると大変励みになります…！
よろしくお願いいたします。

参考文献
=======

本記事を書くにあたっては、下記の記事・書籍を参考にしました。
- **[x + 0.25 - 0.25 = xが成り立たないxとは何か](https://note.com/ruiu/n/ndd60f403e8f2)**
    - スーパープログラマの [Rui Ueyama さん(@rui314)](https://twitter.com/rui314) の書かれた、浮動小数点数に関する面白い記事です。

- **[CPU の創り方](https://www.amazon.co.jp/dp/4839909865)**
    - 4bit の CPU を電子回路から作ってみて CPU の仕組みに切り込んでいく超オススメの書籍です。