フォーラム


ゲスト  

ようこそ ゲスト さん。このフォーラムに投稿するには 登録が必要です。

ページ: [1]
トピック: 実数の計算誤差
DEKO
管理者
投稿数: 2690
実数の計算誤差
on: 2013/06/07 00:35 Fri

Single (単精度浮動小数点 / 32bit) とか Double (倍精度浮動小数点 / 64bit) は整数部の桁が増えると小数部の精度が悪くなります。一般的な金額計算には Currency (通貨型 / 64bit) を使うべきです。Extended (高精度 / 拡張倍精度浮動小数点) は x86 では 80bit ですが、x64 では Double と同じ精度 (64bit) になってしまいます。

[浮動小数点の丸めの問題 (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E3%81%AE%E4%B8%B8%E3%82%81%E3%81%AE%E5%95%8F%E9%A1%8C

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算 (Mr.XRAY)]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm

Currency 型でよく使う関数には、

  • FormatCurr()
  • StrToCurrDef()
  • TryStrToCurr()
  • CurrToStr()

等があります。

関連しますが 「Excel って複雑な計算式は "ちゃんと考えて書かないと" 誤差 (の累積) が酷い事になりますよね」 と言ってもなかなか理解して頂けないばかりか 「お前の作ったアプリと Excel で計算した結果が違うぞ、どうなってるんだ!」 という難癖を付けられる事があります。検算すればどっちが正しいかスグに判るのですが、Excel の計算結果が絶対だと盲信される方は少なからずいらっしゃいますね。

See Also:
[“達人”芳坂和行氏に学ぶ、エクセル「演算誤差」対策講座 (日経PC21)]
http://pc.nikkeibp.co.jp/pc21/special/gosa/

Mr.XRAY
メンバー
投稿数: 192
数値計算の誤差と四捨五入
on: 2014/10/09 21:45 Thu

元のタイトル「実数の計算誤差」と関係がありますので,こちらに投稿します.

コンピュータにおける計算 (数値計算) の誤差と四捨五入に関する話題です.
以下の記事も参考にしてください.

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm

まず,次のコードを実行してみます.
Extended 型の変数に実数値を代入して,その値を指定の小数点以下の桁数で四捨五入 (丸めて) 表示するものです.
実行には,uses に Math が必要です.
以下,全て Windows 7 U64(SP1) + Delpi XE5(UP2) Pro VCL-32 で実行しています.

procedure TForm1.Button1Click(Sender: TObject);
var
E, F : Extended;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add(' 元の値 小数点3桁 小数点2桁');
Memo1.Lines.Add('---------------------------------');

E := 179.9349;
F := 179.9350;

Memo1.Lines.Add(FloatToStr(E) + ' ' +
FloatToStr(SimpleRoundTo(E, -3)) + ' ' +
FloatToStr(SimpleRoundTo(E, -2)));

Memo1.Lines.Add(FloatToStr(F) + ' ' +
FloatToStr(SimpleRoundTo(F, -3)) + ' ' +
FloatToStr(SimpleRoundTo(F, -2)));
end;

  

結果は上の図のようになります.
179.9349 を SimpleRoundTo で小数点 2 桁目で「丸めた」結果は 179.93 となっています.
これは,元の値の小数点 3 桁目の値が 4 です.4 は 5 よりも小さい値です.
したがって,切り捨てられた結果です.

[System.Math.SimpleRoundTo - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.Math.SimpleRoundTo
  
今度は次のコードを実行してみます.
変数に値を直接代入するのではなく,計算結果を代入します.この計算は,計算式は違いますが,数学的には同じ値になります.
同じく uses に Math が必要です.

//-----------------------------------------------------------------------------
// (1)と(2)の計算式は,数学的には等しく,179.935となる
//-----------------------------------------------------------------------------
procedure TForm1.Button2Click(Sender: TObject);
var
A, B, C, D, E, F : Extended;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add(' 計算値 小数点3桁 小数点2桁');
Memo1.Lines.Add('---------------------------------');


A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

Memo1.Lines.Add(FloatToStr(E) + ' ' +
FloatToStr(SimpleRoundTo(E, -3)) + ' ' +
FloatToStr(SimpleRoundTo(E, -2)));

Memo1.Lines.Add(FloatToStr(F) + ' ' +
FloatToStr(SimpleRoundTo(F, -3)) + ' ' +
FloatToStr(SimpleRoundTo(F, -2)));
end;

  

結果の図をみると,計算結果が同じにも関わらず,E の値を小数点 2 桁目まで取得した値が 179.93 となってしまっています.
そこで,Delphi XE2 で実装で実装された浮動小数点用レコードで計算結果の値をバイト値で確認してみました.
Extended 型のレコード型は TExtended80Rec なので,これを使用しています.

[Delphi のデータ型 - RAD Studio]
http://docwiki.embarcadero.com/RADStudio/XE6/ja/Delphi_%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B#.E6.B5.AE.E5.8B.95.E5.B0.8F.E6.95.B0.E7.82.B9.E3.83.87.E3.83.BC.E3.82.BF.E5.9E.8B

//-----------------------------------------------------------------------------
// (1)と(2)の計算式は,数学的には等しく,179.935となる
//-----------------------------------------------------------------------------
procedure TForm1.Button3Click(Sender: TObject);
var
A, B, C, D, E, F : Extended;
ValueByte : TExtended80Rec;
StrText : String;
i : Integer;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add('---------------------------------');


A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

ValueByte := TExtended80Rec(E);
StrText := ' ';
for i := 9 downto 0 do begin
StrText := StrText + IntToHex(ValueByte.Bytes[I], 2) + ' ';
end;
Memo1.Lines.Add(StrText);

ValueByte := TExtended80Rec(F);
StrText := ' ';
for i := 9 downto 0 do begin
StrText := StrText + IntToHex(ValueByte.Bytes[I], 2) + ' ';
end;
Memo1.Lines.Add(StrText);
end;

  

上の結果を見ると,E と F は最後のバイト値,それも最後のビットだけが違います.
つまり,E の元の値は 179.935 ではなく,本当は 179.9349999999999… ということになります.
FloatToStr 関数は,数値を文字列にする時に有効桁数の 15 桁目で「丸め」て文字列を取得します.そのため [計算結果] の値は, E も F も 179.935 と表示されます.
一方,SimpleRoundTo で四捨五入する際は,元の引数の値で「丸め」の操作を行うものと思われます.したがって,179.9349999… を小数点 2 桁で四捨五入すると,は 179.93 となります.

64 ビットのアプリでは,Extended 型は Double 型と同じですので,FormatFloat で文字列に変換すれば実際の数値を確認できます.
以下のそのテスト用のコードです.

[System.SysUtils.FloatToStr - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToStr

//-----------------------------------------------------------------------------
// (1)と(2)の計算式は,数学的には等しく,179.935となる
//-----------------------------------------------------------------------------
procedure TForm1.Button4Click(Sender: TObject);
var
A, B, C, D, E, F : Double;
ValueByte : TExtended80Rec;
StrText : String;
i : Integer;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add('-----------------------------------------');

A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

Memo1.Lines.Add(' E: ' + FormatFloat('000.000000000000000000', E));
Memo1.Lines.Add(' F: ' + FormatFloat('000.000000000000000000', F));
end;

  

結果を見ると,Double 型の計算でさえ少なくとも有効桁数の 15 桁目までは 9 となっています.
したがって,15 桁目で丸めると 179.935 となることが分かります.

Extended 型については以下も参考してください.

[W1066 Extended 型の浮動小数値の精度が失われます。Double 型に丸めました(Delphi) - RAD Studio]
http://docwiki.embarcadero.com/RADStudio/XE7/ja/W1066_Extended_%E5%9E%8B%E3%81%AE%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E5%80%A4%E3%81%AE%E7%B2%BE%E5%BA%A6%E3%81%8C%E5%A4%B1%E3%82%8F%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82Double_%E5%9E%8B%E3%81%AB%E4%B8%B8%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%88Delphi%EF%BC%89

[System.Extended - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE7/ja/System.Extended
 

Mr.XRAY
メンバー
投稿数: 192
Re: 数値計算の誤差と四捨五入
on: 2014/10/10 00:10 Fri

 

引用 Mr.XRAY on 2014/10/09 21:45 Thu
FloatToStr 関数は,数値を文字列にする時に有効桁数の 15 桁目で「丸め」て文字列を取得します.そのため [計算結果] の値は, E も F も 179.935 と表示されます.
一方,SimpleRoundTo で四捨五入する際は,元の引数の値で「丸め」の操作を行うものと思われます.したがって,179.9349999… を小数点 2 桁で四捨五入すると,は 179.93 となります.

   
FloatToStr 関数は,数値を文字列にする時に有効桁数の 15 桁目で「丸め」を行います.
そこで,一度 FloatToStr 関数で 15 桁に丸めた結果の文字列を作成し,その文字列をまた実数に変換すれば,元の数値に 16 桁以上の精度があれば誤差のない結果となります.
そのテストコードです.
今回の計算結果は,すでに 16 桁以上の精度はあることを確認しています.今回のような単純な計算であれば,そのくらいの精度が期待できますが,実際の数値計算で常に期待できるわけではありません.

//-----------------------------------------------------------------------------
// (1)と(2)の計算式は,数学的には等しく,179.935となる
// StrToFloatは15桁で丸めを行うので15桁に丸めた文字列を取得する
// その文字列を再度実数にする
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
A, B, C, D, E, F : Extended;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add(' 計算結果 小数点3桁 小数点2桁');
Memo1.Lines.Add('-----------------------------------------');

A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

E := StrToFloat(FloatToStr(E));
F := StrToFloat(FloatToStr(F));

Memo1.Lines.Add(' E: ' + FloatToStr(E) + ' ' +
FloatToStr(SimpleRoundTo(E, -3)) + ' ' +
FloatToStr(SimpleRoundTo(E, -2)));

Memo1.Lines.Add(' F: ' + FloatToStr(F) + ' ' +
FloatToStr(SimpleRoundTo(F, -3)) + ' ' +
FloatToStr(SimpleRoundTo(F, -2)));
end;

  
下図が実行結果です.

また,指定小数点以上の精度が必要ないか,計算の結果の精度が分かっている場合,次のように,計算の結果をその小数点の桁数に四捨五入してしまう方法も考えられます.

  E := SimpleRoundTo(E, -10);
F := SimpleRoundTo(F, -10);

  
FloatToStrF 関数, FloatToText 関数 や FloatToDecimal 関数で,精度を指定する方法もあります.以下は FloatToStrF を使用した例です.
結果は上の図と同じです.

[System.SysUtils.FloatToStrF - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToStrF
[System.SysUtils.FloatToDecimal - RAD Studio API Documentation:]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToDecimal
[System.SysUtils.FloatToText - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToText

//-----------------------------------------------------------------------------
// (1)と(2)の計算式は,数学的には等しく,179.935となる
// FloatToStrF関数を使用して数値を文字列に変換するコード例
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
A, B, C, D, E, F : Extended;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add(' 計算結果 小数点3桁 小数点2桁');
Memo1.Lines.Add('-----------------------------------------');

A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

Memo1.Lines.Add(' E: ' + FloatToStr(E) + ' ' +
FloatToStrF(E, ffFixed, 10, 3) + ' ' +
FloatToStrF(E, ffFixed, 10, 2));

Memo1.Lines.Add(' F: ' + FloatToStr(F) + ' ' +
FloatToStrF(F, ffFixed, 10, 3) + ' ' +
FloatToStrF(f, ffFixed, 10, 2));
end;]/code]
  
Mr.XRAY
メンバー
投稿数: 192
Re: 数値計算の誤差と四捨五入
on: 2014/10/10 06:52 Fri

次は Double 型でテストしてみます.
既にテストしたように,Double 型での E と F の計算結果は次のようになります.
もちろん,数学的には 179.935 ですが,これがコンピュータによる計算結果なのです.

上の図の結果をみると,やるまでもありませんが,一応,Doubel 型のテスト用コードです.
変数の定義を Double に変えただけです.
動作確認は,同じく Windows 7 U64(SP1) + Delpi XE5(UP2) Pro VCL-32 です.

procedure TForm1.Button1Click(Sender: TObject);
var
A, B, C, D, E, F : Double;
begin
Memo1.Lines.Clear;
Memo1.Font.Name := 'MS ゴシック';
Memo1.Font.Size := 10;
Memo1.Lines.Add(' 計算結果 小数点3桁 小数点2桁');
Memo1.Lines.Add('-----------------------------------------');

A := 389.7;
B := 94.4;
c := 30;
D := 0.35;
E := (A + B + C) * D; //(1)
F := A * D + B * D + C * D; //(2)

Memo1.Lines.Add(' E: ' + FloatToStr(E) + ' ' +
FloatToStr(SimpleRoundTo(E, -3)) + ' ' +
FloatToStr(SimpleRoundTo(E, -2)));

Memo1.Lines.Add(' F: ' + FloatToStr(F) + ' ' +
FloatToStr(SimpleRoundTo(F, -3)) + ' ' +
FloatToStr(SimpleRoundTo(F, -2)));
end;

   
下図が実行結果です.
当然の結果となっています.
[計算結果] の欄で,浮動小数点値を文字列に変換する FloatToStr(E), FloatToStr(F) の結果が 179.934999999999974 となっていないことに注意してください.これは,FloatToStr 関数が内部で 15 桁に丸めているためです.つまり,本当の計算結果ではありません.
Extended でのサンプルでも同じです.結果の表示だけを見ると,数学的な計算結果と同じですが,実際には 16 桁以上の桁では違いがあることになります.そのために,小数点 2 桁で四捨五入した結果に違いがあったわけです.

いろいろテストしましたが,数学的には正しくなくても,数値計算としては当然の結果と言えます.
今回のテストは,非常に単純な計算でしたが,実際の実務での計算はもっと計算式も多く,複雑であると思われます.
その時,結果として取得した値が,数学的に正しいかを把握して検証するの困難でしょう.
今回のように,数学的な計算結果との照合というのは無理があるかも知れません.

コンピュータを使用した計算,特に実数の使用した計算では,常に誤差が伴うことを認識する必要があると思われます.

Mr.XRAY
メンバー
投稿数: 192
Re: 数値計算の誤差と四捨五入
on: 2014/10/14 10:14 Tue

これまでのテストでは,179.934999999999974 を小数点以下 2 桁にした場合,179.93 となっています.
後ろの方の 9999… を考慮してくれれば,179.94 になって,数学的な計算と同じなります.

今回のテストは,数学的な値が分かっています.だからこそ,そういうことも言えます.
しかし,実際の数値計算では,数学的に正しい値が分かっていない場合があります.
むしろ,分かっていない場合の方が多いかも知れません.数学的な計算が困難,時間がかかる等の理由で無理があるから,コンピュータを使う場合があります.

そんな時,小数点 2 桁までは間違いないだろう,でも,小数点 3 桁目も少し信用してやろう.と言う時に

X := SimpleRoundTo(X, -2);
X := RoundTo(X, -2);

 
としてやれば,小数点 2 桁までの「精度」の値とすることができます.
もし,小数点以下 10 桁までは信用できるという場合は,次のようにします.そうすれば,より数学的に近い値が求まる「可能性」があります.そして,「結果」の表示は小数点 2 桁までにしたければ,表示の際に小数点 2 桁までにすることができます.

X := SimpleRoundTo(X, -10);
X := RoundTo(X, -10);

 
「有効桁数」や「精度の桁数」と「表示の桁数」はそれぞれの意味が違います.
「確度」という用語があります.確度というのは,どの程度真の値に近いかという,「正確さ」を表わす場合に使用します.つまり「誤差」と関係してきます.当然ですが,「真の値」が分からなければ意味がありません.
数値計算における精度は,確度とは違います.
数値計算では,小数点以下 2 桁までの精度にしたからと言って,小数点 2 桁までが正確であるという保証はありません.

以下も参考にしてください.

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm
 

ページ: [1]
WP Forum Server by ForumPress | LucidCrew
バージョン: 1.7.5 ; ページロード: 0.046 sec.