web-dev-qa-db-ja.com

Python床除算での丸め誤差

私は浮動小数点演算で丸め誤差が発生することを知っていますが、誰かがこの理由を説明できます:

_>>> 8.0 / 0.4  # as expected
20.0
>>> floor(8.0 / 0.4)  # int works too
20
>>> 8.0 // 0.4  # expecting 20.0
19.0
_

これは、x64ではPython 2および3の両方で発生します。

私が見る限り、これはバグまたは_//_の非常にばかげた仕様のいずれかです。なぜなら、最後の式が_19.0_と評価される理由は何も見当たらないためです。

_a // b_が単にfloor(a / b)として定義されていないのはなぜですか?

[〜#〜] edit [〜#〜]:_8.0 % 0.4_も_0.3999999999999996_に評価されます。それ以来、少なくとも_8.0 // 0.4 * 0.4 + 8.0 % 0.4_は_8.0_に評価されるため、これは結果として生じます

[〜#〜] edit [〜#〜]:これは の複製ではありません浮動小数点演算は壊れていますか? なぜこの特定の操作が丸め誤差(おそらく回避可能)の影響を受けるのか、そして_a // b_がfloor(a / b)として定義されていない理由を尋ねているからです。

[〜#〜] remark [〜#〜]:これが機能しないより深い理由は、床の分割が不連続であるため、無限 条件番号 があり、悪条件の問題になります。床除算と浮動小数点数は基本的に互換性がなく、浮動小数点に対して_//_を使用しないでください。代わりに整数または分数を使用してください。

37
0x539

あなたとkhelwoodがすでに気づいたように、_0.4_は浮動小数点として正確に表すことができません。どうして? 2分の5(_4/10 == 2/5_)であり、有限の2進小数表現はありません。

これを試して:

_from fractions import Fraction
Fraction('8.0') // Fraction('0.4')
    # or equivalently
    #     Fraction(8, 1) // Fraction(2, 5)
    # or
    #     Fraction('8/1') // Fraction('2/5')
# 20
_

しかしながら

_Fraction('8') // Fraction(0.4)
# 19
_

ここで、_0.4_は、浮動小数点リテラル(したがって、浮動小数点2進数)として解釈され、(2進数)丸めが必要であり、のみが有理数に変換されますFraction(3602879701896397, 9007199254740992)、これは厳密には4/10ではありませんが、その後、フロア除算が実行されます。

_19 * Fraction(3602879701896397, 9007199254740992) < 8.0
_

そして

_20 * Fraction(3602879701896397, 9007199254740992) > 8.0
_

結果は20ではなく19です。

同じことがおそらく起こります

_8.0 // 0.4
_

つまり、フロアされた除算はアトミックに決定されているようです(ただし、解釈された浮動小数点リテラルの唯一の近似浮動小数点値に基づいています)。

では、なぜ

_floor(8.0 / 0.4)
_

「正しい」結果を与える?そこにあるため、2つの丸め誤差が互いに相殺されます。 最初 1) 除算が実行され、20.0よりわずかに小さい値が生成されますが、浮動小数点として表現できません。最も近い浮動小数点数に丸められ、それはたまたま_20.0_です。 の場合のみfloor操作が実行されますが、exactly_20.0_に作用するため、もう数を変えないでください。


1) カイルストランド 指摘 として、正確な結果が決定される、丸められたでない実際には実際に何が起こるか2)-level(CPythonのCコードまたはCPU命令)。ただし、それは期待される 3) 結果。

2) 最低 4) しかし、これはそれほど遠くないかもしれません。一部のチップセットは、最初により正確な(ただし正確ではないが、単に2進数があるだけの)内部浮動小数点結果を計算し、次にIEEE倍精度に丸めることによって浮動小数点結果を決定します。

3) Python仕様によって「期待される」。必ずしも私たちの直感によってではない。

4) さて、最低レベルは論理ゲートの上です。半導体がこれを理解できるようにする量子力学を考慮する必要はありません。

25
das-g

@jotasiがその背後にある真の理由を説明しました。

ただし、それを防止したい場合は、基本的に2進浮動小数点表現とは対照的に10進浮動小数点数を表すように設計されたdecimalモジュールを使用できます。

したがって、あなたの場合、あなたは次のようなことをすることができます:

>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')

参照:https://docs.python.org/2/library/decimal.html

11
shiva

少し調査したところ、わかりました issue 。 @khelwoodが示唆したように、0.4は内部的に0.40000000000000002220に評価され、8.0を除算すると20.0よりも少し小さい値になります。次に、/演算子は最も近い浮動小数点数、つまり20.0に丸めますが、//演算子は結果をすぐに切り捨てて、19.0を生成します。

これはもっと速いはずで、「プロセッサに近い」と思いますが、それでもユーザーが望んでいる/期待しているものではありません。

10
0x539

Github上のcpythonでfloatオブジェクトの半公式ソースをチェックした後( https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c )何が起こるか理解できますここに。

通常の除算の場合、float_divが呼び出され(行560)、内部でpython floatsをc -doublesに変換し、除算を実行して、結果のdoubleをpython floatに戻します。 cで8.0/0.4を使用して単純にこれを行うと、次のようになります。

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    printf("%lf\n", floor(vx/wx));
    printf("%d\n", (int)(floor(vx/wx)));
}

// gives:
// 20.000000
// 20

床の分割では、何か他のことが起こります。内部的には、float_floor_div(654行目)が呼び出され、float_divmodが呼び出されます。この関数は、フロア分割された除算を含むpython floatsのタプルと、mod/remainderを返します。 PyTuple_GET_ITEM(t, 0)によって破棄されました。これらの値は次の方法で計算されます(c -doublesへの変換後):

  1. 残りはdouble mod = fmod(numerator, denominator)を使用して計算されます。
  2. 分子は、modによって減らされ、除算を行うときに整数値を取得します。
  3. フロア分割の結果は、floor((numerator - mod) / denominator)を効果的に計算して計算されます
  4. その後、@ Kasramvdの回答ですでに言及されているチェックが行われます。しかし、これは(numerator - mod) / denominatorの結果を最も近い整数値にスナップするだけです。

これが異なる結果をもたらす理由は、浮動小数点演算のためにfmod(8.0, 0.4)0.4ではなく0.0を与えることです。したがって、計算される結果は実際にはfloor((8.0 - 0.4) / 0.4) = 19であり、(8.0 - 0.4) / 0.4) = 19を最も近い整数値にスナップしても、fmodの「間違った」結果によって引き起こされるエラーは修正されません。あなたはそれをcでも簡単にハックすることができます:

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    double mod = fmod(vx, wx);
    printf("%lf\n", mod);
    double div = (vx-mod)/wx;
    printf("%lf\n", div);
}

// gives:
// 0.4
// 19.000000

私は、(@ 0x539の回答のリンクに記載されているように)(numerator//divisor)*divisor + fmod(numerator, divisor) = numeratorの有効性を維持するために、床の除算をこのように計算する方法を選択したと思いますが、これによりfloor(8.0/0.4) != 8.0//0.4の予期しない動作が発生します。

10
jotasi

これは、python(浮動小数点有限表現)に0.4がないためです。これは、実際には0.4000000000000001のような浮動小数点であり、除算の床は19になります。

>>> floor(8//0.4000000000000001)
19.0

しかし、真の除算(/引数が浮動小数点数または複素数の場合、除算結果の妥当な近似を返します。 そして、そのため、8.0/0.4の結果は20です。実際には、引数のサイズに依存します(Cのdouble引数の場合)。 (最も近い浮動小数点数に丸めません

グイド自身による pythons integer Division floors の詳細を読んでください。

また、浮動小数点数に関する完全な情報については、この記事を読むことができます https://docs.Oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

興味がある人のために、次の関数はCpythonのソースコードで浮動小数点数の真の除算タスクを実行するfloat_divです。

float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

関数PyFloat_FromDoubleによってどの最終結果が計算されるか:

PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        free_list = (PyFloatObject *) Py_TYPE(op);
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}
9
Kasramvd