計算機:誤差の問題
計算機:誤差の問題
ここではunityや市販されている計算機、プログラムコード上の数式計算で発生する誤差について計算機科学的に考察する
unityでfloat型を使い計算する事を考えてみる
<float型の特徴>
- 浮動小数点型。小数点の計算を扱える
- 有効数字7桁。浮動小数点で7桁。つまり 1234.567f や 0.00001234567f といった形の数字が扱える
- 0.00001234567f 等は内部で 1.234567*10^-5f として変換され、これで浮動小数点有効数字7桁となっている
- 12.3456789f 等の場合、7桁以降は動作保証外となる。利用者側としては 12.34567f として扱う必要がある
以下のような計算を行い、その値の変化率と真値率を見てみる
これは元の値からどれぐらい「変化したか」と、どれぐらい「正しい値になったか」の割合を見ている
using UnityEngine;
using System.Collections;
public class CalcError1 : MonoBehaviour
{
float a, b, c; float trueValue1, trueValue2, trueValue3, trueValue4;
void Start () { a = 1234.567f; b = 1234.55f; c = 0.001f; trueValue1 = 2469.117f; trueValue2 = 1234.568f; trueValue3 = 0.017f; trueValue4 = 1234.566f;
print (string.Format ("①{0}+{1}={2} : 変化率{3:p7}:真値率{4:p}", a, b, a + b, (a + b) / a, (a + b) / trueValue1)); print (string.Format ("②{0}+{1}={2} : 変化率{3:p7}:真値率{4:p}", a, c, a + c, (a + c) / a, (a + c) / trueValue2)); print (string.Format ("③{0}-{1}={2} : 変化率{3:p7}:真値率{4:p}", a, b, a - b, (a - b) / a, (a - b) / trueValue3)); print (string.Format ("④{0}-{1}={2} : 変化率{3:p7}:真値率{4:p}", a, c, a - c, (a - c) / a, (a - c) / trueValue4)); }
}
出力
①1234.567+1234.55=2469.117 : 変化率199.9986000 %:真値率100.00 %
②1234.567+0.001=1234.568 : 変化率100.0001000 %:真値率100.00 %
③1234.567-1234.55=0.01696777 : 変化率0.0013744 %:真値率99.81 %
④1234.567-0.001=1234.566 : 変化率99.9999200 %:真値率100.00 %
a と b は値を比べると小さな差を持つ
a と c は極端に大きな差を持つ関係になっている
この各計算結果の”変化率”に注目すると③の計算だけが他と比べ大きな変化率を示している事がわかる
また真値率は約0.2%のズレを示し有効数字2桁目以降の値が本来の値でなくなっている
これは大きな計算誤差となるという事を意味している。この③の計算の特徴を整理すると以下になる
良く似た値のふたつの数字の引算をすると、有効数字先頭数桁が正常でそれ以降間違っている誤差のある値を得る事になる
従って上記のような条件の計算は避ける事が必須である
例えば③の様なケースを無視して、そのまま計算を漸化的に繰り返したりすると、どうなるか?
考えられるのは先頭1~2桁が正常でそれ以降の値が狂った計算を繰り返すことで、末尾の数字に誤差が蓄積していく事により
ある桁以降からまったく正確性が無い答えを得ることになる
上記のコードの場合、③は今後どんな計算をしても10^-3以降の数字に永遠に正確性は無くなる
その様な値をもとに逆数をとったり、指数や累乗根の計算を行うと、さらにその誤差は有効数字先頭へと影響を広げる
誤差に対しての対処方法
以下の事に留意する事で誤差の発生を事前に防ぐ事が出来る
- 大きさの近いふたつの数字の引算の操作を避ける
- どうしても事情により避けられない上で精度が必要な場合は有理数計算の数式になるようにする(これにより浮動小数点有効桁数内でより近似な計算ができる)
- 同条件で根号が関わってくる場合は、根号に対する有理化(一般に分母の有理化と呼ばれる)操作を行い、数式から「大きさの近いふたつの数字の引算」をなくす
<補足>
根号に対する有理化は分子に行っても良い。また計算機科学では計算の運用上、分母の有理化は必ず行う必要のある操作ではないと考える事
(条件反射的に中学算数で教えられた分母の有理化をした場合、「大きさの近いふたつの数字の引算の操作」が発生する場合がある。これは式を良くみて判断するべき)
計算の内容が浮動小数点有効桁数を横断する程、長い場合は素直により多い桁数が扱える型の使用を検討する float型 ⇒ double型 に変更する等
有理数計算の例
このような式があったとして答えを求めたい時、unityで、どのように数式をコード化すればよいか
using UnityEngine;
using System.Collections;
public class CalcTest2 : MonoBehaviour
{
float a, b; void Start () { a = 39f / 321f; //これがある"傾き"を表していた場合・・・ b = 81f / 667f; print (string.Format ("通常の計算方法 :{0}", a - b)); print (string.Format ("有理数として計算:{0}", ((39f * 667f - 81f * 321f) / (321f * 667f)))); }
}
出力
通常の計算方法 :5.605072E-05
有理数として計算:5.604674E-05
出力結果から有理数として通分し計算した方が真値に近い事がわかる
a がプログラムコード上、ある傾きを表していた場合、分母、分子が変数であれば、それにあてはめて考えコードを組むと精度を向上させられる
根号に対する有理数計算の例
たとえば下記のような根号の計算をする場合を考える
この場合、式の計算の動きをよく観察すると①も②も、「大きさの近いふたつの数字の引算」が
根号内で行われている事がわかる。具体的には2-√○の部分。この箇所に対し有理化の式変形を利用する事で計算精度を向上させる事が出来る
<有理化の式変形(因数分解の「和と差の積」、1の変形も併用)>
using UnityEngine;
using System.Collections;
using System;
public class CalcTest3 : MonoBehaviour
{
void Start () { //float計算の場合 print ("通常計算 :" + Mathf.Sqrt (2f - Mathf.Sqrt (4f - 0.05f * 0.05f))); print ("有理化した計算:" + 0.05f / Mathf.Sqrt (2f + Mathf.Sqrt (4f - 0.05f * 0.05f))); //double計算の場合 print ("通常計算 :" + Math.Sqrt (2d - Math.Sqrt (4d - 0.00005d * 0.00005d))); print ("有理化した計算:" + 0.00005d / Math.Sqrt (2d + Math.Sqrt (4d - 0.00005d * 0.00005d))); }
}
<出力>
通常計算 | 0.02500267 |
有理化した計算 | 0.02500195 |
通常計算 | 2.50000010342546E-05 |
有理化した計算 | 2.50000000019531E-05 |
- 最終更新:2015-02-03 03:17:37