06-07-2017 01:22 AM - 編集済み 06-07-2017 01:23 AM
初めて質問させていただきます。
始まり値(i)、終わり値(e)、間隔(s) を与えると、i から s ごとに増加し eを超えないところまでの値を持つ1次元配列(v[n])を返す関数を書こうとしています。
例えば、i = 1, e = 1.8, s = 0.1 なら v = [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8] を返すものです。ここで、i, e, s は浮動小数点数とします。
添付のvi で動作すると思ったのですが、
i = 1, e = 1.7, s = 0.1 などとした場合、期待した配列([1, ... , 1.6, 1.7]) が得られません。
除算や for ループに整数値を入れていないところに問題がありそうなのですが、どのように書くべきでしょうか?
06-07-2017 02:56 AM
コンピュータが小数を扱う際の罠ですね。
「0.1」は実は「0.100000000000000006」なので、丸め込みの際に1~1.7だと「-6.99999999999999911」となり切下げ整数で-6になってしまうのが原因ですね。
最も近い値に丸め込んでは如何でしょう。
06-07-2017 08:43 AM
ありがとうございます。
切り下げていたところを最も近い値に丸め込むことで解決しました。
勉強になりました。
06-08-2017 08:59 AM - 編集済み 06-08-2017 09:01 AM
横槍で恐縮です。
TAKASU様のご教示の通りで基本的には解決していると思いますが、この件は、2進数のコンピュータで10進小数を扱う際の根源的問題ですので、以前、似たようなスレッドで回答したこともありますが、ご参考になさって下さい(そのスレッドの途中からコメントしています)。
10進数なら割り切れて見える実数も2進表現では割り切れないものが多かったり、格納できるビット数が有限なことから、時々、直観的でない挙動をします。
「微小なもの」が効く例だけでなく、微小で片づけられないもっと「普通な」場面でも現れてしまうことがあり、その最たる挙動の1つが以下の例です。
信じられないかも知れませんが、0.6×0.6≠0.4×0.9なのです。
doubleでもsingleでもこうなります。ベタ打ちの0.36とイコールなのは0.6×0.6の方です。
これは、浮動小数点の演算結果を、イコールで判定してはいけない例です。
(微小な計算許容誤差εを設定して、差の絶対値がそのε以下、として記述する)
これらを、内部表現をもとにビット列で表すと以下のようになり、結果が異なるのが分かります。
(ビット数が有限なので打ち切られていますが、真の2進数は無限小数ですのでご留意を)
同じ理由で、doubleの0.36と、singleの0.36は「=」につないでもイコールになりません(singleのほうがビットが足りないので)。
大きな値に、小さな値を加算し続ける場合も、桁落ちが累積して、意図しないことになります。大きな値どうしを引き算する場合も桁落ちして同様です。
なお、今回のようにdiscreteな扱いで良い場合は「丸める」のも手ですね。
ご存知かも知れませんが、「丸める(最近接)」は、端数が0.5のとき、上がるか下がるかが独特ですのでお気を付け下さい。四捨五入ではなく「最近接の偶数」へ丸められます。つまり、1.5は2になりますが、2.5も2になります。3.5は4になります。今回の場合は整数に極めて近いところにあると思うので問題にはならないと思いますが・・
06-08-2017 09:56 PM
もう解決されたようなので蛇足ですが、Full DevelopmentSystemをお持ちなら、ランプパターンVIを使えば今回のような動作を簡単に実装できます。
06-09-2017 03:12 AM
すごくわかりやすい例示で説明してくださりありがとうございます。
リンク先も興味深く読ませていただきました。
"普通の"小数どうしの割り算や比較で直観的でない値が出るのは驚きでした。
「最近接に丸める」の独特な振舞いも教えていただいて初めて知りました。
今回の場合も
・微小値εを足して切り上げ整数化
・値の比較は差分の絶対値が計算許容誤差ε以下かで判定
することで、解決できそうです。
まだ混乱しているのでご返信に時間がかかってしまったのですが、
適当な範囲内でそれぞれのパラメータをしらみつぶしに変化させて、おそらくうまく動くものがかけたと思います。
06-09-2017 03:28 AM
情報ありがとうございます。残念ながらFull DevelopmentSystem は持っていないようなのですが、組み込みVIが使えるならそれを使うべきですね。
参考までにランプパターンVI でも、上でみられたようなことは注意する必要があるという認識でよろしいでしょうか。
06-09-2017 08:08 AM
ランプパターンVIはお使いにはなれないのですね。失礼しました。
ランプパターンVIでは今回のような問題は発生しません。
下記ヘルプページの下部にある、「ランプパターンの詳細」にパターン生成のアルゴリズムが書いてありますので、参考になさってください。そこに載っている式に従ってご自身で実装すれば、同様の動作を実現できます。
ただ、そちらに変えても処理速度はあまり変わらないでしょうし、今お使いのもので他に問題がないのであれば、お作りになったものをそのまま使っても大丈夫だと思います。
06-09-2017 10:17 AM - 編集済み 06-09-2017 10:20 AM
ランプパターン関数でも、問題は発生しますよ。
ランプパターン関数は、start、endと、sample数を指定しますが、start以外は、end-startを(sample数-1)で割ったものの0倍・1倍・2倍・・・をstartに加算していますから、配列の最後の数はendになるはずのところ、厳密には「引数で指定したend」と一致はしないのです。
例として、start=0.01、end=0.99、として、適当なサンプル数のランプパターンを生成し、その最終要素(論理的には引数のendと等しいはず)が、本当に引数のendと等しいかを確かめると、サンプル数8までは一致しますが、サンプル数9で、不一致となります。
また、この不一致具合はいつも同じではなく、例えばサンプル数99(要は0.01刻み)の時には上記の3倍くらいの開きとなり、
サンプル数100だとまたちょっと違った差になり、大小も逆転します。
ランプパターンは、引数のendと、返ってきた配列の最後の数は、一致比較できない(大きいかも知れないし、小さいかも知れない)、という意味では、使い方によっては問題が起きるかも知れない、ということです。
(今回の用途での「ランプパターン関数で問題が起きない」とは、指定したサンプル数ぶんだけ、指定したend近傍までの値でちゃんと配列を生成する、という意味合い)
06-09-2017 11:19 AM
いろいろためしてみましたが、M. Shiraishi様のおっしゃるとおりですね。
あまりそれほど下の桁を意識することがなかったので、気づいていませんでした。
知識不足で適当なことを言って申し訳ありません。勉強します。