「数値型の計算誤差」の版間の差分
ページの作成:「Category:チュートリアル AliceScriptの数値型は、倍精度浮動小数点数数値型と定められており、この規格はIEEE754として標準化さ…」 |
編集の要約なし |
||
(同じ利用者による、間の18版が非表示) | |||
1行目: | 1行目: | ||
{{Alert|この記事のより新しい版が[https://docs.wsoft.ws/products/alice/tutorial/calculation-error/ WSOFTDocs]にあります。AliceScriptWikiでは、この記事はもう更新されません。||04}} | |||
[[Category:チュートリアル]] | [[Category:チュートリアル]] | ||
AliceScriptの数値型は、倍精度浮動小数点数数値型と定められており、この規格はIEEE754として標準化されています。 | AliceScriptの数値型は、倍精度浮動小数点数数値型と定められており、この規格はIEEE754として標準化されています。 | ||
13行目: | 15行目: | ||
知的好奇心旺盛なあなたのために、もう少し説明します。例えば10進数の<code>0.1</code>を2進数に変換すると<code>0.0001100110011…</code>となり、<code>0011</code>が永遠と循環します。そのため<code>0.1</code>を倍精度浮動小数点数数値型に格納するには、適当な桁で丸める必要があります。このとき、最も近い偶数に値を丸めます。その結果として<code>0.1</code>は数値型では2進数で<code>0.0001100110011001100110011001100110011001100110011001101</code>となります。これを10進数に戻す<code>0.1000000000000000055511151231257827021181583404541015625</code>となり、<code>0.1</code>ではなくなります。 | 知的好奇心旺盛なあなたのために、もう少し説明します。例えば10進数の<code>0.1</code>を2進数に変換すると<code>0.0001100110011…</code>となり、<code>0011</code>が永遠と循環します。そのため<code>0.1</code>を倍精度浮動小数点数数値型に格納するには、適当な桁で丸める必要があります。このとき、最も近い偶数に値を丸めます。その結果として<code>0.1</code>は数値型では2進数で<code>0.0001100110011001100110011001100110011001100110011001101</code>となります。これを10進数に戻す<code>0.1000000000000000055511151231257827021181583404541015625</code>となり、<code>0.1</code>ではなくなります。 | ||
ただし整数は、有効桁数15桁の範囲内であれば、正確に格納できます。また小数であっても、k/2^n (k,nは整数)で表すこともできる小数は2進数で表現できるため、正確に格納できます。 | |||
なおこのような誤差は、AliceScript固有のものではありません。IEEE 754の2進浮動小数点形式を採用しているシステムでは、同じことが起こりえます。 | |||
=回避策= | |||
==許容範囲を決めて値を比較する== | |||
二つの浮動小数点数数値型数を比較するとき、例えばそれらが等しいかを調べるときに<code>==</code>を使うのは危険です。両者が全く等しい場合のみ等しいと判断するのではなく、「両者の差の絶対値がある程度であれば等しいと認める」など許容範囲を決めて比較する方が安全です。次の例は、許容範囲を定めt、<code>0.000001</code>未満の差の場合に等しいと判断します。 | |||
import “Alice.Math”; | |||
///二つの小数が等しいと認められるかどうかを評価します | |||
///パラメータ a:一方の値 | |||
/// b:もう一方の値 | |||
/// tolerance:許容できる誤差の最大の値(この数値は常に正) | |||
function NumEqual(number a,number b,number tolerance) | |||
{ | |||
return (math_abs(a - b) < tolerance); | |||
} | |||
var numA = 0.1 + 0.2; | |||
var numB = 0.3; | |||
///許容範囲 | |||
var tolerance = 0.000001; | |||
if(NumEqual(numA,numB,tolerance)) | |||
{ | |||
print(“二値は等しいと認められました”); | |||
} | |||
==一度整数にした後で変換する== | |||
他には、例えば一度整数にしてから計算する方法も考えられます。次の例では、二つの数を整数にすることができる数<code>dis ** 10</code>を求めて二数を整数にした後、計算を行うことで誤差を防ぎます。ただしこの場合でも、有効数字15桁以上の数は正確に扱うことができません。 | |||
import “Alice.Math”; | |||
///与えられた数の小数点以下の桁数を取得します | |||
function GetDisitsUnder1(number num) | |||
{ | |||
var priceString = price.ToString().TrimEnd('0'); | |||
int index = priceString.IndexOf('.'); | |||
if (index == -1){return 0;} | |||
return priceString.Substring(index + 1).Length; | |||
} | |||
///二つの小数が等しいかどうかを判断します | |||
function NumEqual(number numA,numbner numB) | |||
{ | |||
//numA,numBの小数点以下の桁数(すなわち、10の何乗倍すれば整数になるか) | |||
var nAd = GetDisitsUnder1(numA); | |||
var nBd = GetDisitsUnder1(numB); | |||
//二つの小数の小数点以下桁数のうち多い方 | |||
var dis = math_max(nAd,nBd); | |||
//numA,numBを整数化したもの | |||
var rA = numA * (dis ** 10); | |||
var rB = numB * (dis ** 10); | |||
return (rA == rB); | |||
} | |||
///二つの小数の和を求めます | |||
function Sum(number numA,number numB) | |||
{ | |||
//numA,numBの小数点以下の桁数(すなわち、10の何乗倍すれば整数になるか) | |||
var nAd = GetDisitsUnder1(numA); | |||
var nBd = GetDisitsUnder1(numB); | |||
//二つの小数の小数点以下桁数のうち多い方 | |||
var dis = math_max(nAd,nBd); | |||
//numA,numBを整数化したもの | |||
var rA = numA * (dis ** 10); | |||
var rB = numB * (dis ** 10); | |||
//整数化されたものの計算結果 | |||
var result = rA + rB; | |||
return (result / (dis ** 10)); | |||
} | |||
print(NumEqual(0.1+0.2,0.3));//出力例:true | |||
print(Sum(0.1,0.2));//出力例:0.3 |
2023年6月29日 (木) 01:32時点における最新版
この記事のより新しい版がWSOFTDocsにあります。AliceScriptWikiでは、この記事はもう更新されません。 |
AliceScriptの数値型は、倍精度浮動小数点数数値型と定められており、この規格はIEEE754として標準化されています。 しかしこの数値型で小数の計算をしているとき、その計算結果が期待通りでないことがあります。この記事では、その理由について説明します。
計算誤差の例[編集]
0.1 + 0.2 == 0.3
数学では、上記の式は正しくtrue
と評価されますし、ほとんどの開発者も、それが正しいことを期待します。
では、AliceScriptでこの式を評価してみます。
print(0.1 + 0.2 == 0.3);
このコードを実行すると、false
が出力されます。
理由[編集]
これらの計算誤差はバグではありません。AliceScriptの数値型のような浮動小数点数数値型は、値を2進数で格納していますが、ほとんどの10進数の小数は2進数で表すことができず、その近似値として表現されます。近似値の誤差が例に示したような計算誤差として現れます。
知的好奇心旺盛なあなたのために、もう少し説明します。例えば10進数の0.1
を2進数に変換すると0.0001100110011…
となり、0011
が永遠と循環します。そのため0.1
を倍精度浮動小数点数数値型に格納するには、適当な桁で丸める必要があります。このとき、最も近い偶数に値を丸めます。その結果として0.1
は数値型では2進数で0.0001100110011001100110011001100110011001100110011001101
となります。これを10進数に戻す0.1000000000000000055511151231257827021181583404541015625
となり、0.1
ではなくなります。
ただし整数は、有効桁数15桁の範囲内であれば、正確に格納できます。また小数であっても、k/2^n (k,nは整数)で表すこともできる小数は2進数で表現できるため、正確に格納できます。
なおこのような誤差は、AliceScript固有のものではありません。IEEE 754の2進浮動小数点形式を採用しているシステムでは、同じことが起こりえます。
回避策[編集]
許容範囲を決めて値を比較する[編集]
二つの浮動小数点数数値型数を比較するとき、例えばそれらが等しいかを調べるときに==
を使うのは危険です。両者が全く等しい場合のみ等しいと判断するのではなく、「両者の差の絶対値がある程度であれば等しいと認める」など許容範囲を決めて比較する方が安全です。次の例は、許容範囲を定めt、0.000001
未満の差の場合に等しいと判断します。
import “Alice.Math”; ///二つの小数が等しいと認められるかどうかを評価します ///パラメータ a:一方の値 /// b:もう一方の値 /// tolerance:許容できる誤差の最大の値(この数値は常に正) function NumEqual(number a,number b,number tolerance) { return (math_abs(a - b) < tolerance); } var numA = 0.1 + 0.2; var numB = 0.3; ///許容範囲 var tolerance = 0.000001; if(NumEqual(numA,numB,tolerance)) { print(“二値は等しいと認められました”); }
一度整数にした後で変換する[編集]
他には、例えば一度整数にしてから計算する方法も考えられます。次の例では、二つの数を整数にすることができる数dis ** 10
を求めて二数を整数にした後、計算を行うことで誤差を防ぎます。ただしこの場合でも、有効数字15桁以上の数は正確に扱うことができません。
import “Alice.Math”; ///与えられた数の小数点以下の桁数を取得します function GetDisitsUnder1(number num) { var priceString = price.ToString().TrimEnd('0'); int index = priceString.IndexOf('.'); if (index == -1){return 0;} return priceString.Substring(index + 1).Length; } ///二つの小数が等しいかどうかを判断します function NumEqual(number numA,numbner numB) { //numA,numBの小数点以下の桁数(すなわち、10の何乗倍すれば整数になるか) var nAd = GetDisitsUnder1(numA); var nBd = GetDisitsUnder1(numB); //二つの小数の小数点以下桁数のうち多い方 var dis = math_max(nAd,nBd); //numA,numBを整数化したもの var rA = numA * (dis ** 10); var rB = numB * (dis ** 10); return (rA == rB); } ///二つの小数の和を求めます function Sum(number numA,number numB) { //numA,numBの小数点以下の桁数(すなわち、10の何乗倍すれば整数になるか) var nAd = GetDisitsUnder1(numA); var nBd = GetDisitsUnder1(numB); //二つの小数の小数点以下桁数のうち多い方 var dis = math_max(nAd,nBd); //numA,numBを整数化したもの var rA = numA * (dis ** 10); var rB = numB * (dis ** 10); //整数化されたものの計算結果 var result = rA + rB; return (result / (dis ** 10)); } print(NumEqual(0.1+0.2,0.3));//出力例:true print(Sum(0.1,0.2));//出力例:0.3