【C#】浮動小数の誤差とイプシロンを用いた同値比較について【Epsilon】

スポンサーリンク
PC創作
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜
マイケル
マイケル
今日はプログラミングに関するお話です!
いきなりですが、下記コードはどのような結果になると思いますか?
        /// <summary>
        /// 0.1刻みで1.0までカウントする
        /// </summary>
        private void Count()
        {
            // 1.0になるまでループ
            float count = 0.0f;
            while (true)
            {
                // countに0.1を加算
                count += 0.1f;
                Console.WriteLine("足しました->" + count.ToString());
                // 1.0になったら抜ける
                if (count == 1.0f)
                {
                    Console.WriteLine("1になったので抜けます");
                    break;
                }
            }
        }
↑1.0になるまで加算するプログラム
エレキベア
エレキベア
1になるまで加算したら処理が終了するクマ?
マイケル
マイケル
それでは結果をみてみましょう・・・
01 count NG
↑終わらない
マイケル
マイケル
処理が終了せずにひたすら加算し続けます・・・。
エレキベア
エレキベア
恐ろしいクマ・・・。
スポンサーリンク

浮動小数の誤差

マイケル
マイケル
これは ==を使って同値比較を行なっている ため起こる問題です。
floatのような浮動小数では若干の誤差が出てしまう ため、同一ではない判定になってしまうんですね・・・。
エレキベア
エレキベア
count == 1.0f の部分クマね・・・
マイケル
マイケル
なんで誤差が発生してしまうのか、少し考えてみよう!

浮動小数とは

マイケル
マイケル
まず、そもそも浮動小数とは何かについて!
これは コンピュータで小数同士の計算を楽に行うための表現方法 です。
マイケル
マイケル
おおざっぱにいうと、

符号部 → 正の値(0)、負の値(1)
指数部 → 計算のベース値 (2のべき乗)
仮数部 → ベース値から近づける値 (ベース値を2のべき乗で除算)

となります!
エレキベア
エレキベア
(全然わからんクマ・・・)
マイケル
マイケル
float型のような単精度浮動小数(32ビット)と、
double型のような倍精度浮動小数(64ビット)では、それぞれ下記のような構成になっています。
ScreenShot 2021 05 30 13 51 37
↑半精度浮動小数の構成(float型)
ScreenShot 2021 05 30 13 51 48
↑倍精度浮動小数の構成(double型)
マイケル
マイケル
具体例として、13.75を単精度浮動小数で表したものを下記の図に表します!
ScreenShot 2021 05 30 13 15 07
↑13.75を浮動小数で表現したもの
マイケル
マイケル
指数部でベースとなる8を設定して、
仮数部で5.75を加算することで表現しています。
マイケル
マイケル
一点気をつけないといけないのは、指数部は小数も表現するため、通常 0〜255 の範囲が -127〜128の範囲になっていること!
計算する時には127を引いてべき乗する必要があります!
エレキベア
エレキベア
なんとなく分かったような分からないようなクマ・・・
マイケル
マイケル
とりあえずコンピュータは2進数(2のべき乗)で表現している
ということだけ抑えておこう!

なぜ誤差が起きるのか

マイケル
マイケル
それではなぜ浮動小数では何故誤差が起きてしまうのかについて、考えてみよう!
調べたところ、こちらの記事で分かりやすく説明してくれていました!

マイケル
マイケル
小数は 0になるまで2で乗算し続ける ことで2進数に変換するけど、
ほとんどの小数は綺麗に0にならないから誤差が出てしまうということですね・・・。
エレキベア
エレキベア
0.1〜0.9の中で綺麗に表せるのは0.5だけ
というのは驚きクマ・・・
マイケル
マイケル
試しに0.1がどんな値になってるのか見てみましょう!
適当に70桁ほど指定して表示してみます。
// 誤差を表示
float countF = 0.1f;
double countD = 0.1d;
Console.WriteLine("{0:F70}", countF);
Console.WriteLine("{0:F70}", countD);
[出力結果]
0.1000000014901161193847656250000000000000000000000000000000000000000000
0.1000000000000000055511151231257827021181583404541015625000000000000000
マイケル
マイケル
float型、double型でそれぞれこれだけの誤差が出ているのが分かります・・・
エレキベア
エレキベア
恐ろしいクマ・・・
マイケル
マイケル
浮動小数で表現するとどうなるかも見てみましょう!
こちらは下記サイトのツールを使用させていただきました!

ScreenShot 2021 05 30 13 45 25
↑0.1を浮動小数に変換した結果
マイケル
マイケル
どちらも2^-4=0.0625 をベースとして、
なんとか近づけようとしている
ことが分かりますね。
エレキベア
エレキベア
誤差が出る理由はなんとなく分かったクマ

スポンサーリンク

計算機イプシロンを用いた比較

マイケル
マイケル
この誤差をどのように比較するのがいいかについてですが、
会社で 計算機イプシロンという値を使って比較するのがよいという話を伺いました!

マイケル
マイケル
「1より大きい最小の数と1との差」のことらしく、
このイプシロンの範囲を許容範囲とする考え方です!
エレキベア
エレキベア
そんな定義があったクマね

C#で用意されているイプシロン

マイケル
マイケル
調べたらC#には Single.Epsilon、Double.Epsilon
が用意されている!
みたいだったので使ってみたのですが・・・
        /// <summary>
        /// 0.1刻みで1.0までカウントする
        /// </summary>
        private void Count()
        {
            // イプシロンを取得
            float eps = Single.Epsilon;

            // 1.0になるまでループ
            float count = 0.0f;
            while (true)
            {
                // countに0.1を加算
                count += 0.1f;
                Console.WriteLine("足しました->" + count.ToString("G17"));
                // 1.0になったら抜ける
                if (MathF.Abs(count - 1.0f) <= eps)
                {
                    Console.WriteLine("1になったので抜けます");
                    break;
                }
            }
        }
↑C#のイプシロンを使った比較
マイケル
マイケル
なかなか思うような比較結果にならず・・・
エレキベア
エレキベア
なんでクマ・・・
マイケル
マイケル
どうやらC#に用意されているイプシロンは
計算機イプシロンではなく、「浮動小数点演算の丸の相対誤差の上限(丸め誤差発生時にズレるであろう最大値)」らしく・・・。

マイケル
マイケル
どうやら自分で求めるしかなさそうです・・・。
エレキベア
エレキベア
ややこしすぎるクマ・・・。
マイケル
マイケル
ちなみにUnityで用意されている Math.Epsilonもこちらと同じ値のようでした・・・。

計算機イプシロンを求める

マイケル
マイケル
というわけで自分で求めてみましょう!
調べたところ 変数をひたすら2で割り続け、ゼロになった瞬間のひとつ前を捉えるというアルゴリズムが有名なようです!
        /// <summary>
        /// 計算機イプシロン(float)を求める
        /// </summary>
        /// <returns>計算機イプシロン(float)</returns>
        private float GetFloatEpsilon()
        {
            float eps = 1.0f;
            while (1.0f + eps / 2.0f != 1.0f)
            {
                eps /= 2.0f;
            }
            Console.WriteLine(eps);
            return eps;
        }

        /// <summary>
        /// 計算機イプシロン(double)を求める
        /// </summary>
        /// <returns>計算機イプシロン(double)</returns>
        private double GetDoubleEpsilon()
        {
            double eps = 1.0d;
            while (1.0d + eps / 2.0d != 1.0d)
            {
                eps /= 2.0d;
            }
            Console.WriteLine(eps);
            return eps;
        }
↑イプシロンを求めるアルゴリズム
[出力結果]
1.1920929E-07
2.220446049250313E-16
マイケル
マイケル
出力結果を見た感じ、うまく求められていそうです!
エレキベア
エレキベア
やったクマ〜〜〜

計算機イプシロンを使って比較

マイケル
マイケル
それではこちらを使って比較してみましょう!
        /// <summary>
        /// 0.1刻みで1.0までカウントする
        /// </summary>
        private void Count()
        {
            // イプシロンを取得
            float eps = GetFloatEpsilon();

            // 1.0になるまでループ
            float count = 0.0f;
            while (true)
            {
                // countに0.1を加算
                count += 0.1f;
                Console.WriteLine("足しました->" + count.ToString("G17"));
                // 1.0になったら抜ける
                if (MathF.Abs(count - 1.0f) <= eps)
                {
                    Console.WriteLine("1になったので抜けます");
                    break;
                }
            }
        }
↑イプシロンを用いた比較
[出力結果]
足しました->0.10000000149011612
足しました->0.20000000298023224
足しました->0.30000001192092896
足しました->0.40000000596046448
足しました->0.5
足しました->0.60000002384185791
足しました->0.70000004768371582
足しました->0.80000007152557373
足しました->0.90000009536743164
足しました->1.0000001192092896
1になったので抜けます
マイケル
マイケル
うまく比較できました!!
エレキベア
エレキベア
やったクマ〜〜〜〜〜!!!!

だめだった・・・

マイケル
マイケル
これで解決!!
・・・と思いましたが、だめなパターンがありました。
マイケル
マイケル
下記のように、10まで加算したり、0.01刻みで加算したりなど、
加算する回数が多くなれば誤差も大きくなり、イプシロンの範囲に収まらなくなるみたいです・・・。
        /// <summary>
        /// 0.1刻みで10.0までカウントする
        /// </summary>
        private void Count()
        {
            // イプシロンを取得
            float eps = GetFloatEpsilon();

            // 10.0になるまでループ
            float count = 0.0f;
            while (true)
            {
                // countに0.1を加算
                count += 0.1f;
                Console.WriteLine("足しました->" + count.ToString("G17"));
                // 10.0になったら抜ける
                if (MathF.Abs(count - 10.0f) <= eps)
                {
                    Console.WriteLine("10になったので抜けます");
                    break;
                }
            }
        }
↑加算する回数が多くなるとNG
エレキベア
エレキベア
どうすればいいクマ・・・

どうするのが正しい?

マイケル
マイケル
調べてみると、下記記事でも似たようなことが書かれていて、
複数回演算することによって誤差が積み重なることがあるため、
実際のプロジェクトでは本来の数値よりもすこし大きめの数値で定義して運用する
とのことでした…。

マイケル
マイケル
つまり、少し大きめの許容範囲の誤差を自分で定義するしかなさそうですね・・・。
        /// <summary>
        /// 0.1刻みで10.0までカウントする
        /// </summary>
        private void Count()
        {
            // ★イプシロンを定義
            float eps = 1E-05f;

            // 10.0になるまでループ
            float count = 0.0f;
            while (true)
            {
                // countに0.1を加算
                count += 0.1f;
                Console.WriteLine("足しました->" + count.ToString("G17"));
                // 10.0になったら抜ける
                if (MathF.Abs(count - 10.0f) <= eps)
                {
                    Console.WriteLine("10になったので抜けます");
                    break;
                }
            }
        }
↑許容範囲のイプシロンを大きめに設定する
マイケル
マイケル
これでうまく比較はできました!
許容範囲はそのケースによって調整するのがよさそうです・・・。
エレキベア
エレキベア
なんかあいまいな感じもするクマね・・・。
マイケル
マイケル
その他の方法だと、丸めて比較したり、10、100倍して型変換して比較
する方法もありそうですね・・・。
エレキベア
エレキベア
比較一つも大変クマね・・・。
スポンサーリンク

おわりに

マイケル
マイケル
というわけで今回は浮動小数の同値比較についてでした!
どうだったかな?
エレキベア
エレキベア
結局パッとしない答えになってしまったクマね・・・
マイケル
マイケル
正直もっといい方法もありそうだよね・・・
(何かいい方法があれば、ぜひコメント蘭に指摘お願いします!)
マイケル
マイケル
とりあえず忘れちゃいけないのは
浮動小数を扱う時には誤差に注意すること!!
これは頭の片隅にいれておきましょう!
エレキベア
エレキベア
確かによく使うクマだし、
一歩間違えればバグに繋がるクマからね
マイケル
マイケル
数値同士の比較がうまくいかないときには今回の話を思い出しましょう。
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!!
エレキベア
エレキベア
クマ〜〜〜〜〜

【C#】浮動小数の誤差とイプシロンを用いた同値比較について【Epsilon】 〜完〜

コメント