t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA 指数表記と誤差

【注意】この記事は推測を含み、事実とは異なる可能性があります。

とあるブログ(既に閉鎖)で、Exp関数に1を与えてi乗してLogを取ると、iが8と16のときに不一致になるとの報告があった。
数学的には、一致するはずである。

これは面白い発見だ。

実際に検証してみた。

Public Sub 誤差テスト()
    For i = 1 To 100
        Debug.Assert i = Log(Exp(1) ^ i)
    Next
End Sub

上記のコードをF5で実行すると、iが8のときに中断する。
また、中断モードでF5継続すると、次は16でとまる。

なぜ8と16なのか。

私の推測では、ただ偶然に8と16が絶妙な誤差を検知しただけではないかと思う。

VBエディタで以下を入力してみて欲しい。

Debug.Print 8.000000000000001

カーソルがその行から外れると、Debug.Print 8#という風に勝手に変換される。
#はDouble型であることをあらわす数値の接尾記号だ。

つまりただのDuble型の8である。
0.000000000000001が吹っ飛んでしまうのだ。

Int型の8と比較してみても一致する。

Debug.Print 8# = CInt(8) 'Trueになる。

次に以下のように書いてみる。

Debug.Print 8 + 0.000000000000001

コード上は勝手に変換されないが、実行してみると、ただの8が出力される。

しかし!
出力したら8になるくせに、これは8と比較するとFalseになるのだ。

    Debug.Print 8 + 0.000000000000001 = 8 'Falseになる。

ためしにあとひとつ0を増やしてみよう。

    Debug.Print 8 + 0.0000000000000001

これはコード上でそのまま表示できず、以下のように自動変換されてしまう。

Debug.Print 8 + 1E-16

Excelを使っていても E-20とかいった表記は見たことがあるかもしれない。
これは小数点がいくつズレているかをあらわす方法で、指数表記という表記法だ。
0が多すぎる場合に表示される。

1E-16とはつまり、 1 \times 10^{-16} (10のマイナス16乗)ということ。
この-16をベキ指数と呼ぶ。

VBAのようなプログラミング言語では 10^{-16}のようにベキ指数を上付きの小書きで表現することができないので、代替表記としてEを用いる。
英語で「ベキ指数」のことをExponentというので、その頭文字のEである。

E-に続く数値が大きい方が、数は小さい。
VBAではE-15以上が普通の小数点で表記され、E-16未満が指数表記となる。

ためしに次のように書いてみると良く分る。

Debug.Print 1e-15

他にもいくつか試してみよう。
5e-1は、0.5
98e-3は、0.098

ちなみに、マイナスだけでなく、プラスで大きな数も表現できる。
15e+1は、150である。
1e+100は100000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000である。


これで大体指数表記の意味はつかめたと思う。

さて、ここからが本題である。
以下のように、8に0.000000000000001(つまり1E-15)を足すと、表面上は8なのに8と一致しない変な数になる。

    Debug.Print 8 + 0.000000000000001 = 8 'False

ところが1E-16だと、無視されて完全な8になってしまう。

    Debug.Print 8 + 1E-16 = 8 'True

コンピューターで扱えるケタ数に限界があるので、足される値の桁数に差がありすぎると、小さいほうの値が消滅してしまうのだ。
これを情報落ちという。

はじめに紹介したプロシージャであるが、

Public Sub 誤差テスト()
    For i = 1 To 100
        Debug.Assert i = Log(exp(1) ^ i)
    Next
End Sub

おそらくたまたま、8と16の時にたまたま情報落ちしない微妙な誤差(捨てられるでもなく、表示されるでもない)が発生してしまったのだと思う。
そもそもexp(1)は無理数なので、必ず誤差は生じている。むしろガンガン情報落ちが発生するはず。
その誤差が検知される大きさになって現れたのが、たまたま8と16だったということではないかと思う。

さらに実験を続ける。
Debug.PrintでExp(1)を表示させると、「2.71828182845905」とでる。

これをi乗してLogをとっていく。

Public Sub 誤差テスト()
    For i = 1 To 100
        Debug.Print Log(2.71828182845905 ^ i)
    Next
End Sub

結果、絶妙な誤差を含む数値が取れた。

 1 
 2 
 3.00000000000001 
 4.00000000000001 
 5.00000000000001 
 6.00000000000001 
 7.00000000000001 
 8.00000000000001 
 9.00000000000002 
 10 
 11 
 12 
…省略

一部小数点の無いものが混じっているが、完全な整数になったわけではない。
その根拠として、以下のようにiと比較すると、1・2・10・11・12も、すべてFalseになる。

Public Sub 誤差テスト()
    For i = 1 To 100
        Debug.Print Log(2.71828182845905 ^ i) = i
    Next
End Sub

前述の記事には「もっとたくさんの不一致が出そうなものだ」とのコメントが書かれていたが、おそらく上記の意味である。

私の推測まとめ

本来は無理数を扱っている時点で誤差は避けられないが、それが情報落ちを引き起こして結果的に整数に戻ってしまっているためにほとんどの数値で一致するのではないかと思う。
ところが、8と16でたまたま情報落ちしない程度に誤差が発生したため、整数との不一致が発生したということか。

数値が大きくなればなるほど情報落ち誤差は発生しやすくなるため、iが一定を超えると情報落ちによりほぼ整数化されてしまうはず。
よって比較的小さい桁において8と16の2つの例外が集中しているのではないかと思う。

Public Sub CDec誤差テスト()
    For i = 1 To 100
        Debug.Print CDec(Log(2.71828182845905 ^ i)) = i
    Next
End Sub

さて、誤差を最初から抑制するには、Decimal型を使えばよい。
以下のようにすると結果はすべてTrueとなる。

Public Sub CDec誤差テスト()
    For i = 1 To 100
        Debug.Print CDec(Log(exp(1) ^ i)) = i
    Next
End Sub

exp(1)が無理数である以上、完全な計算は無理であるが、情報落ちしてしまうほど小さな誤差に収まるために計算結果は整数化されてしまうものと思われる。

これはあくまで私の推論である。

もっと詳しい方の前で得意げに話すと恥をかくかもしれないので注意。

真相を知りたければデバッガでメモリの動きを覗いてみればよい。
ただ、私にはそんな技術も、そんなことが出来るのかどうかも知らない。
誰か凄い人が、私の説の正しさを裏付けてくれるのを待とうと思う。

余談

コンピューターで少数を扱ううえでは、いつも誤差がついてまわる。
今回のような症状はVBAに特有のものではなく、一般的にコンピューターというのは誤差を孕んでいる。
言語処理系によってはうまく回避しているものもあるのかもしれないが、この1件に関してはVBAがしょぼいから発生するわけではなく、コンピューターの仕組みに根ざした深ーい問題であると思う。

つまり私が言いたいのは、
thom.hateblo.jp

ということである。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。