web-dev-qa-db-ja.com

4 * 0.1の浮動小数点値がPython 3で見栄えがよいのに3 * 0.1は見栄えが悪いのはなぜですか?

ほとんどの小数には正確な浮動小数点表現がないことを知っています( 浮動小数点演算は壊れていますか? )。

しかし、なぜ4*0.10.4としてうまく印刷されるのかわかりませんが、両方の値が実際にはい10進表現を持つ場合、3*0.1は印刷されません。

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
152
Aivar

簡単な答えは、3*0.1 != 0.3は量子化(ラウンドオフ)エラーによるものです(一方、4*0.1 == 0.4は2の累乗で乗算するのが通常「正確な」操作であるためです)。

Pythonの.hexメソッドを使用して、数値の内部表現を表示できます(基本的に、10進数の近似ではなくexactバイナリ浮動小数点値)。これは、内部で何が起こっているかを説明するのに役立ちます。

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1は0x1.999999999999a×2 ^ -4です。末尾の「a」は数字10を意味します-つまり、0.1のバイナリ浮動小数点は非常にわずか 0.1の「正確な」値よりも大きくなります(最後の0x0.99は切り上げられるため) 0x0.aへ)。これに4の2の累乗を掛けると、指数は(2 ^ -4から2 ^ -2に)シフトしますが、それ以外の数は変わらないため、4*0.1 == 0.4です。

ただし、3を掛けると、0x0.99と0x0.a0(0x0.07)のわずかな差が0x0.15エラーに拡大し、最後の位置に1桁のエラーとして表示されます。これにより、0.1 * 3はごくわずか丸められた値0.3より大きくなります。

Python 3のfloat reprは、round-trippableになるように設計されています。つまり、表示される値は元の値に正確に変換可能である必要があります。したがって、0.30.1*3をまったく同じ方法で表示することはできません。または、2つの異なる数値は、ラウンドトリップ後に同じ結果になります。その結果、Python 3のreprエンジンは、わずかに明らかなエラーのあるものを表示することを選択します。

297
nneonneo

repr(およびPython 3のstr)は、値を明確にするために必要な桁数を出力します。この場合、乗算の結果3*0.1は0.3(16進数で0x1.3333333333333p-2)に最も近い値ではなく、実際には1 LSB高い(0x1.3333333333334p-2)ので、区別するためにより多くの数字が必要です。 0.3から。

一方、乗算4*0.1doesは0.4に最も近い値(16進数で0x1.999999999999ap-2)を取得するため、追加の桁は必要ありません。

これは非常に簡単に確認できます。

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

上記の16進表記を使用したのは、ニースでコンパクトであり、2つの値のビットの違いを示しているためです。あなたはこれを使用して自分で行うことができます(3*0.1).hex()。むしろそれらのすべての小数の栄光でそれらを見たいなら、ここに行きます:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')
75
Mark Ransom

他の回答からの簡単な結論を以下に示します。

Pythonのコマンドラインでフロートをチェックするか、印刷する場合、文字列表現を作成する関数reprを通過します。

バージョン3.2以降、Pythonのstrおよびreprは複雑な丸めスキームを使用します。これは、可能であれば見栄えの良い小数を優先しますが、フロートとその文字列表現。

このスキームは、repr(float(s))の値が、浮動小数点数として正確に表現できない場合(例:s = "0.1")

同時に、すべてのフロートxに対してfloat(repr(x)) == xが保持されることを保証します

22
Aivar

Pythonの実装に固有のものではありませんが、浮動小数点から10進数の文字列関数に適用する必要があります。

浮動小数点数は本質的には2進数ですが、有効数字の固定制限がある科学表記法です。

基数と共有されない素数係数を持つ任意の数の逆数は、常に点の繰り返し表現になります。たとえば、1/7には10と共有されない7の素因数があり、したがって10進表現が繰り返されます。1/ 10にも素数2と5があり、後者は2と共有されません;これは、ドットポイントの後の有限ビット数で0.1を正確に表すことができないことを意味します。

0.1には正確な表現がないため、近似値を小数点文字列に変換する関数は通常、0.1000000000004121のような直感的な結果が得られないように特定の値を近似しようとします。

浮動小数点は科学表記法であるため、基数の累乗による乗算は、数値の指数部のみに影響します。たとえば、10進表記では1.231e + 2 * 100 = 1.231e + 4、2進表記では1.00101010e11 * 100 = 1.00101010e101です。基数の非累乗を掛けると、有効数字も影響を受けます。たとえば、1.2e1 * 3 = 3.6e1

使用するアルゴリズムによっては、有効数字のみに基づいて一般的な小数を推測しようとする場合があります。 0.1と0.4は両方とも、バイナリで同じ有効数字を持ちます。これは、フロートが本質的に(8/5)(2 ^ -4)と(8/5)(2 ^ -6)それぞれ。アルゴリズムが8/5のsigfigパターンを10進数の1.6として識別する場合、0.1、0.2、0.4、0.8などで機能します。また、float 3をfloat 10で除算するなど、他の組み合わせのマジックsigfigパターンがある場合があります。統計的には、10で除算することで形成される可能性が高いその他の魔法のパターン。

3 * 0.1の場合、最後のいくつかの有効数字は、フロート3をフロート10で除算した場合とは異なる可能性があり、精度の損失の許容値に応じて、アルゴリズムは0.3定数のマジックナンバーを認識できません。

編集: https://docs.python.org/3.1/tutorial/floatingpoint.html

興味深いことに、同じ最も近い近似2進小数を共有する多くの異なる10進数があります。たとえば、数値0.1および0.10000000000000001および0.1000000000000000055511151231257827021181583404541015625はすべて3602879701896397/2 ** 55で近似されます。これらの10進数値はすべて同じ近似値を共有するため、不変式eval(repr(x) )== x。

Float x(0.3)がfloat y(0.1 * 3)に正確に等しくない場合、repr(x)はrepr(y)に正確に等しくない場合、精度の低下に対する許容度はありません。

5
AkariAkaori