WarpspaceはCaveduck.ioをオンデバイス・低コストで動かすために、モデルを小さくするさまざまな実験を行っています。この記事はその軽量化シリーズの第1回(#1 データ型)です。続く#2 量子化#3 プルーニング編では、ここで扱ったデータ型を使って実際にモデルを小さくし、その結果を自分の手で測定します。

データ型はなぜこんなに増えたのか

数年前まで、ディープラーニングの数値はただfloat32一つで十分でした。ところが最近の論文やモデルカードを見ると、FP16BF16FP8 (E4M3/E5M2)INT8INT4FP4NF4… ありとあらゆるデータ型があふれています。名前を見ただけでは何が何やら、なぜこんなに種類があるのか分かりません。

理由は単純です。低ビット演算はとにかく安いからです。45nmプロセスを基準にすると、演算一つあたりのエネルギーコストはおおよそ次のとおりです。

演算エネルギー (pJ)
8-bit int ADD0.03
32-bit int ADD0.1
32-bit float ADD0.9
8-bit int MULT0.2
32-bit float MULT3.7

8ビット整数の乗算は32ビットfloatの乗算より約18倍安く、加算は30倍安いです。さらに、メモリから値を一つ読み出すコストは、乗算・加算一回の数百倍も高くつきます。ですから同じモデルであっても、数値をより少ないビットに収めるほど電力も、メモリも、帯域幅も節約できます。上に並んだ多種多様なデータ型は、結局のところ「何ビットを、どこに使うのか」という問いに、それぞれ異なる答えを出した結果なのです。

問題は、ビットを減らすと表現できる数値の種類が減り、その分だけ精度や範囲を失うということです。そのため、フォーマットごとに符号・指数・仮数へビットをどう配分するかで性格が分かれます。この記事では、その配分ルールを — ビットが実際にどんな数値へ解釈されるのか — を一つずつ紐解いていきます。

各データ型ごとに、ビットを直接クリックできるウィジェットを用意しました。0と1を押してひっくり返すと、計算式と結果値がリアルタイムに変わります。説明を目で読むよりも、自分でビットを触ってみるほうが、はるかに早く感覚がつかめます。


1. 整数 (Integer)

符号なし整数 (Unsigned Integer)

最も単純です。$n$個のビットそれぞれが$2^k$の桁の値を持ち、立っているビット($=1$)の桁の値をすべて足し合わせます。

$$\text{value} = \sum_{i=0}^{n-1} b_i \cdot 2^i$$

表現範囲は$[0,\ 2^n - 1]$。8ビットなら0から255までです。

下のウィジェットでビットを押してみてください。デフォルト値00110001は$2^5 + 2^4 + 2^0 = 49$です。

符号あり整数 (Signed Integer)

負の数を表現するには符号が必要です。二つの方式があります。

符号・絶対値 (Sign-Magnitude) — 先頭のビットを符号として使い($0$=正、$1$=負)、残りで大きさを表現します。直感的ですが致命的な欠点があります。0000000010000000どちらも0です(+0と−0)。0が二つあるとハードウェアが煩雑になります。範囲は$[-2^{n-1}+1,\ 2^{n-1}-1]$。

2の補数 (Two’s Complement) — 現代のコンピュータが実際に使っている方式です。アイデアは簡単です。先頭ビットの桁の値を負にします。つまりMSBの重みが$+2^{n-1}$ではなく$-2^{n-1}$になります。

$$\text{value} = -b_{n-1}\cdot 2^{n-1} + \sum_{i=0}^{n-2} b_i \cdot 2^i$$

こうすると0は00000000一つだけになり、範囲は$[-2^{n-1},\ 2^{n-1}-1]$と負の側が一つ広くなります。下の11001111は$-2^7 + 2^6 + 2^3 + 2^2 + 2^1 + 2^0 = -49$です。MSBを消すとどう変わるか押してみてください。


2. 固定小数点 (Fixed-Point)

整数だけでは小数を収められません。最も簡単な拡張は、小数点の位置を固定しておくことです。ビット列をそのまま2の補数の整数として読み取り、定めた分だけ$2^{-f}$でスケールします。

$$\text{value} = (\text{整数として読んだ値}) \times 2^{-f}$$

下は8ビットを整数部4ビット・小数部4ビット($f=4$)に分けたものです。各ビットの重みが$2^3, 2^2, \dots, 2^0, 2^{-1}, \dots, 2^{-4}$と続きます。00110001は整数として読むと49で、$49 \times 2^{-4} = 3.0625$です。

固定小数点は単純ですが限界がはっきりしています。小数点の位置が固定なので、表現できる大きさの幅(range)が狭いです。 非常に大きな数と非常に小さな数を同時に扱うのが困難です。この問題を解決するのが、次の主役、浮動小数点です。


3. 浮動小数点 (Floating-Point) — IEEE 754

浮動小数点は名前のとおり、小数点が浮遊します。 科学的記数法($1.5 \times 10^3$)の2進数バージョンだと考えればよいでしょう。ビットを三つの部分に分けます。

  • Sign (符号) — 1ビット
  • Exponent (指数) — 大きさの規模を定める → 表現範囲(range) を決定
  • Fraction (仮数部) — 細かい値を定める → 精度(precision) を決定

一般的な(normal)数の解釈式は次のとおりです。

$$\text{value} = (-1)^{\text{sign}} \times (1 + \text{Fraction}) \times 2^{\text{Exponent} - \text{bias}}$$

ここでbiasは指数を負の値まで表現するためのオフセットで、$2^{e-1}-1$($e$=指数のビット数)です。そして仮数の前の**$(1 + \cdots)$** に注目してください。正規数は常に先頭に暗黙の1が付きます(これを「implicit leading 1」と呼びます)。

ちょっと用語整理 — 仮数 / Fraction / Mantissa / Significand

この部分の用語は文献ごとに少しずつ混ざって使われるので混乱しやすいです。上の式の$1.\text{Fraction}$全体、つまり有効数字の部分を指す正式名称はSignificand(有効数字) です。

  • Fraction (仮数部) — ビットに実際に格納される小数部分だけを指します。ウィジェットで黄色く塗られたビットがまさにこれ。小数点以下の値$0.\text{b}_1\text{b}_2\cdots$です。
  • Significand (有効数字) — 暗黙の1まで含めた**$1.\text{Fraction}$全体**。実際に掛けられる有効数字です。
  • Mantissa (仮数) — 歴史的には対数表の小数部分を呼んだ言葉ですが、浮動小数点では普通Fractionと同じ意味でゆるく使われます。IEEE 754標準そのものは「Significand」を正式用語とし「mantissa」は推奨しませんが、現場では依然として「仮数/mantissa」がよく使われます。

まとめるとSignificand = 1 + Fraction、そして日常的に「仮数(mantissa)」と言えば、たいてい格納されるFractionを指します。この記事ではビットに収まるフィールドをFraction(仮数部) と呼びます。

下は32ビット単精度(FP32)です。符号1 + 指数8 + 仮数23 = 32ビット、biasは127。デフォルト値は$0.265625 = (1 + 0.0625) \times 2^{125-127}$を表現します。指数ビットを一つずつ押して、値が2倍ずつ跳ねるのを確認してみてください。

特別な値たち: 0、無限大、NaN、そしてsubnormal

式を見ると奇妙な点が一つあります。$(1 + \text{Fraction})$のせいで、正規数では0を表現できません。 そこでIEEE 754は指数フィールドを特別な信号として使います。

指数(Exponent)Fraction = 0Fraction ≠ 0解釈
00…0 (=0)$\pm 0$subnormal$(-1)^s \times \text{Fraction} \times 2^{1-\text{bias}}$
00…1 ~ 11…0normalnormal$(-1)^s \times (1+\text{Fraction}) \times 2^{\text{Exp}-\text{bias}}$
11…1 (=max)$\pm\infty$NaN

肝心なのは指数がすべて0のときです。このときは暗黙の1を外し($1+\text{Fraction} \to \text{Fraction}$)、指数を$2^{1-\text{bias}}$に固定します。こうして作られるのがsubnormal(非正規)数で、0付近の非常に小さな値を細かく埋めてくれます。逆に指数がすべて1のときは無限大(仮数0)とNaN(仮数 ≠ 0)になります。

上のFP32ウィジェットで実際に作ってみてください。

  • 指数をすべて1に (0 11111111 0…0) → +∞
  • そこから仮数のどれかのビットを立てるとNaN
  • すべて0に0
  • 指数だけ0にしたまま仮数を少し立てると → 非常に小さなsubnormal値

指数の幅が広いほど表現範囲が広がり、仮数の幅が広いほど精度が上がります。Exponent → Range、Fraction → Precision。 この一行が、以降すべての低精度フォーマット設計における核心的なトレードオフです。


4. 半分のサイズへ: FP16とBF16

FP32は正確ですが32ビットも食います。ディープラーニングはそこまで精密である必要がない場合が多いので、16ビットフォーマットを使います。同じ16ビットなのに、ビットをどこに配分するかで分かれます。

FP16 (IEEE 754 Half Precision)

指数5 + 仮数10。biasは15。精度(仮数)に多く投資した配分です。下は$1\,10001\,1100000000$、つまり$-(1+0.75)\times 2^{17-15} = -7.0$です。

BF16 (Google Brain Float)

指数8 + 仮数7。biasは127。FP16と総ビット数は同じですが、指数をFP32とまったく同じ8ビットに保ちます。つまり表現範囲はFP32と同一で、その代わりに精度を犠牲にしました。学習中の勾配のように値の規模(scale)が大きく揺れ動く状況でもオーバーフローの心配がないため、人気があります。

下は$2.5 = (1 + 0.25)\times 2^{1}$をBF16で表現したものです($0\,10000000\,0100000$)。

二つのウィジェットで指数ビット数の違いを感じてみてください。BF16は指数のセルが8マスあるので非常に大きい/小さい数まで届きますが、仮数が7マスしかないため値がまばらです。FP16はその逆です。


5. さらに下へ: FP8とFP4

FP8 (E4M3 / E5M2)

8ビット浮動小数点は最新のハードウェア(例: Nvidia Hopper/Blackwell)がサポートしています。二つの配分が標準のように使われます。

  • E4M3 — 指数4 + 仮数3。精度優先。順伝播(forward)の重み・活性値に主に使います。INFがなくS.1111.111だけをNaNとして使います。表現可能な最大の正規値は$448$。
  • E5M2 — 指数5 + 仮数2。範囲優先。逆伝播(backward)の勾配のように規模の大きな値に使います。IEEEのようにINFとNaNを持ちます。

まずE4M3から。biasは7です。デフォルト値0 0111 000は$ (1+0)\times 2^{7-7} = 1.0$です。

E5M2。bias 15。指数5マスでずっと広い範囲を収めますが、仮数が2マスしかないため値の間隔が粗いです。

INT4とFP4

最も極端です。4ビットで表現可能な値はわずか16個だけです。この16個をどう配置するかがフォーマットごとに異なります。

INT4 — 2の補数の整数。$-8$から$7$まで均等な間隔で並びます。

FP4は指数/仮数の配分によって値の分布が変わります。指数ビットを増やすほど0付近は密に、外側は粗く、非均等(non-uniform) に広がります。

  • E1M2 — 指数1 + 仮数2。整数に最も近いです (bias 0)。0111 = $(1+0.75)\times 2^{1-0} = 3.5$。
  • E2M1 — 指数2 + 仮数1 (bias 1)。0111 = $(1+0.5)\times 2^{3-1} = 6$。
  • E3M0 — 指数3 + 仮数0 (bias 3)。仮数がないため事実上2の冪だけを表現します。0111 = $(1+0)\times 2^{7-3} = 16$。randomボタンを押して、値が$\dots, 4, 8, 16$のように指数的に広がっていくのを確認してみてください。

この四つのフォーマットの「表現可能な16個の値」を数直線の上に一度に打ってみると、性格の違いが一目で分かります。INT4は一定間隔、FP4は指数ビットを増やすほど0付近が密になり外側が広がります。

4ビットフォーマットの表現可能な16個の値: 均等(INT) vs 非均等(FP)

ここで面白い観察が一つ出てきます。ニューラルネットの重みの分布は、たいてい0付近に集中していて裾が長いです。 だとすれば、値を均等に並べるINTよりも、0付近を密に埋めるFP系のほうが、同じ4ビットでも実際の値をより上手く合わせられます。どんな分布にはどんなフォーマットが合うのか — データ型の選択がそのまま精度につながる理由です。(この観察を実際のモデルで測定するのは量子化編で扱います。)


まとめ

ここまでが、ディープラーニングで出会うデータ型が実際に何を意味するのかについての話です。整理すると、

  • 整数 / 固定小数点 — 値が均等な間隔。単純だが範囲が狭い。
  • 浮動小数点 — 指数で範囲を、仮数で精度を分け合う。0付近は密で外側は粗い。
  • ビットを減らすということ = 表現可能な値の個数を減らすということ。FP32の約43億個から、FP4の16個まで。

これでFP16BF16FP8 E4M3INT4のような名前を見れば、頭の中にビット配置が描けるようになるでしょう。ディープラーニングにおける「どのデータ型を使うか?」は、もはや些細な実装ディテールではなく、モデルをどれだけ小さく速く、それでいて正確に動かせるかを分ける設計の選択になったのです。

次の記事では、これらのデータ型を武器に、すでに学習済みの32ビットモデルを実際に2〜8ビットへ小さくしてみて — 精度がどれだけ保たれるのか、サイズがどれだけ減るのか — 自分でコードを走らせて測定します。

👉 ニューラルネット量子化編: 32ビットモデルを2ビットまで小さくしてみる