浮動小数点計算の誤差

3月中、冬かと思うような寒さが続いていましたが、ようやく暖かくなってきた感がありますね。
花粉対策として、とりあえずマスクをするようにしたいと思います。


さて表題の件です。
プログラムで計算をする際、floatやdoubleといった浮動小数点型を使用すると
誤差が出るというのは周知の事実だと思います。

浮動小数点数内部表現シミュレーターhttps://tools.m-bsys.com/calculators/ieee754.phpで、
例えば「11.9」を単精度表現(float)に変換すると「11.899999618530273」になりますね。

しかし、プログラムではある程度自動で丸めてくれるため、誤差発生に気づきづらくなっています。
以下、一例です。(環境はJava1.8)

float _f01 = 9.7f / 10.0f;
System.out.println(_f01); // 0.96999997

float _f02 = _f01 * 100.0f;
System.out.println(_f02); // 97.0

100を積算した結果、「96.999997」になるかと思いきや「97.0」になりました。
丸められていることが分かります。

では誤差を気にしなくとも良いのかというと、もちろんそんな事はありません。
先ほどの結果は、計算前数値の末尾が7であるため、切り上げられたものと考えられます。

では末尾が4以下になる数値はあるのか。0.1~25.5の値を0.1刻みで加算しながら、10で割ってみます。

float _f03 = 0.0f;
for (int _i = 1; _i < 256; _i++) {
	_f03 = (float)(_i / 10.0f);
	System.out.println(_f03 / 10.0f);
}

// 結果
0.01
0.02
0.030000001
0.04
0.05
0.060000002
0.07
0.08
0.089999996
0.1
0.11
0.120000005
0.13
0.14
0.15
0.16
0.17
0.17999999
0.19
0.2
0.21
0.22
0.22999999
0.24000001
0.25
0.26
0.27
0.28
0.29000002
0.3
0.31
0.32
0.32999998
0.34
0.35
0.35999998
0.37
0.38
0.39000002
0.4
0.41
0.42
0.43
0.44
0.45
0.45999998
0.46999997
0.48000002
0.49
0.5
0.51
0.52
0.53000003
0.54
0.55
0.56
0.57
0.58000004
0.59000003
0.6
0.61
0.62
0.63
0.64
0.65
0.65999997
0.66999996
0.68
0.69
0.7
0.71
0.71999997
0.73
0.74
0.75
0.76
0.77
0.78000003
0.79
0.8
0.81000006
0.82
0.83000004
0.84
0.85
0.86
0.87
0.88
0.89
0.9
0.91
0.91999996
0.93
0.93999994
0.95
0.96000004
0.96999997
0.98
0.98999995
1.0
1.01
1.02
1.03
1.04
1.05
1.0600001
1.0699999
1.08
1.0899999
1.1
1.11
1.12
1.13
1.14
1.15
1.1600001
1.17
1.1800001
1.1899999
1.2
1.21
1.22
1.23
1.24
1.25
1.26
1.27
1.28
1.29
1.3
1.3100001
1.3199999
1.33
1.3399999
1.35
1.36
1.37
1.38
1.39
1.4
1.4100001
1.42
1.4300001
1.4399999
1.45
1.46
1.47
1.48
1.49
1.5
1.51
1.52
1.53
1.54
1.55
1.5600001
1.5699999
1.58
1.5899999
1.6
1.61
1.6200001
1.6299999
1.64
1.65
1.6600001
1.6700001
1.68
1.6899999
1.7
1.71
1.72
1.7299999
1.74
1.75
1.76
1.7700001
1.78
1.79
1.8
1.8100001
1.82
1.8299999
1.8399999
1.85
1.86
1.8700001
1.8799999
1.89
1.9
1.9100001
1.9200001
1.93
1.9399999
1.95
1.96
1.97
1.9799999
1.99
2.0
2.01
2.02
2.03
2.04
2.05
2.06
2.0700002
2.08
2.09
2.1
2.1100001
2.1200001
2.1299999
2.1399999
2.15
2.16
2.17
2.1799998
2.19
2.2
2.21
2.22
2.23
2.24
2.25
2.26
2.27
2.28
2.29
2.3
2.31
2.3200002
2.33
2.34
2.35
2.3600001
2.3700001
2.3799999
2.3899999
2.4
2.41
2.42
2.4299998
2.44
2.45
2.46
2.47
2.48
2.49
2.5
2.51
2.52
2.53
2.54
2.55

どうやら「9.4」が怪しい感じです。先ほどの例に、「9.4」を入れて試してみます。

float _f01 = 9.4f / 10.0f;
System.out.println(_f01); // 0.93999994

float _f02 = _f01 * 100.0f;
System.out.println(_f02); // 93.99999

見事に誤差が発生しました。この状態で小数点以下を切り捨てたらおしまいですね。

今回調査した範囲だと、1/255の確率で誤差発生という感じですが、範囲が広がる毎に誤差率も上がります。
対策は必須と言えます。

対策としては、JavaであればBigDecimalクラスを使う方法などもあるようですが、
そもそも浮動小数点の状態で計算しないようにしたほうが良いでしょう。

先ほどの例も、「94」の状態であれば、計算で誤差は発生しません。

float _f01 = 94.0f;
float _f02 = _f01 * 100.0f;
System.out.println(_f02); // 9400.0

浮動小数点値が必要な場合、可能な限り整数の状態で計算した後、最後に桁合わせを行うようにしたいですね。

以上