使用 epsilon 比較浮點數是否不夠科學?
寫代碼的人應該都知道,比較兩個浮點數是否相等一般是用 if(abs(f1-f2)&
1. 4e10 與 4.0000004e10
2. 4e-10 與 5e-10
這兩種情況下我們如果都使用epsilon=1e-6的話,會得出第一組兩個數不相等,第二組兩個數相等 的結果。但是這與數學上來說的話結果應該是相反的。是不是應該在使用epsilon比較前,在epsilon的階數上加上絕對值較大的那個比較數的階數,然後再做比較,這樣比較合理。
各位大神怎麼看這個問題?我百度過了並沒有找到類似的做法,所以來知乎一問。2015/11/7補充
感覺下面的討論已經越來越跑偏了,實在忍不住出來補充一下,怪我問問題的時候沒有分析清楚。
我在討論到是怎麼樣在寫程序的時候處理好浮點數比較這個問題。我認為@周星星 的答案就是我要的,所以看完他的回答我就意識到了,在程序里做浮點數相等的比較是一件很愚蠢的事情,不到萬不得已都不要這麼做就對了。
當然也要感謝@劉海洋 把誤差怎麼來的分析的很徹底了,確實這個是用數值分析能講清楚的。但是原因並不是解決方案。
所以我就沒搞明白後面的一堆說我沒學過數值分析或者覺得這個問題是數值分析第一節課內容就能解決、但是又沒有解釋的人是怎麼理解的,如果你沒有寫過程序不知道計算機怎麼存浮點,那我是可以理解的。還是你們覺得數值分析太高大上了,我沒法理解?
還有就是有人覺得4e10 和 4.0000004e10的比較沒有意義的,你們可以自己試一下把(用c語言中float類型變數) 0.4 累加 10次,再乘以1e10,然後看一下輸出的是什麼,這樣你們就知道我到底在問什麼了。
最後真的建議大家看一下 @周星星 的答案,個人認為那才是工程上比較合理有效的解決方案。
所以大學課堂會有數值分析這麼一門課。
所以數值分析這門課的一個核心主題就是誤差分析。
嚴肅的科學計算中,是先列出公式、演算法,由公式推導理論誤差上界,然後按誤差的界來進行比較、判斷終止條件等。
比如說,計算機浮點數運算,IEEE 754 要求硬體上加減乘除、求餘數、開平方根、整數類型轉換等幾個運算是「精確舍入」的,即計算結束後要讓二進位浮點數的最後一位精度都是準確舍入的,這要求計算過程要 CPU 內部使用比浮點數表示精度更多的位數來計算中間結果。中間表示要多幾位?開平方的迭代要進行幾次?是要靠嚴格的數學分析完成的。浮點數的表示和基本運算的分析可以讀著名論文 [1]。複雜一些的演算法則要讀數值分析的教材/文獻。
然而誤差分析很難。一方面,即使是簡單的四則運算,其誤差分析已經很繁瑣或者困難了(比如考慮 (a-b) * c,而 a、b 很接近)。另一方面,對很複雜的演算法即使是計算數學的專家也很難給出精確的誤差上界,能給出的上界又太大而不實用。所以現實中也會使用一些經驗值,特別是當理論誤差上界沒什麼用時。
不過誤差分析很難並不是完全無視誤差問題的借口。像在所有地方無腦寫 abs(x-y) &< 1e-6 這種做法當然是錯誤的。如果 x 和 y 絕對值都很大,會不會永遠無法判斷相等?絕對值都很小,會不會永遠當它們相等?實際上,當考慮帶誤差的計算時,就不應該是判斷兩個數是否相等,而是要根據兩個數的來源,判斷兩個計算結果的差是否落在誤差上界之內。
舉個例子說吧,如果兩個浮點數 x, y 都是從整數轉化來的,或者只與整數做過加減法運算,那麼兩個數的浮點表示就是 0 誤差的。此時要比較兩個數相等就直接用 x == y 沒有問題。
再舉個例子,如果 x 是牛頓法解多項式方程根的迭代結果,y 是上一次的迭代結果,要判斷想得到 8 位有效數字什麼時候應該停止迭代運算。牛頓法的理論分析告訴我們演算法是二階收斂,而多項式計算(四則運算)的浮點舍入誤差遠小於 8 位十進位精度,因此只需要考慮演算法誤差 |x-y|,那麼比較的應該是 |x-y| / x &< 1e-8,即兩次計算的相對誤差。
我倒…… 數值分析第一節課就講了相對誤差和絕對誤差…… T_T
浮點數本來就是一個數學工具,拿來做重要的邏輯運算當然是不合適的。重要的底層軟體里是不會出現浮點數的,都是整形。
從另一個角度說,如果你要解決一個數學問題,浮點數當然不是不能用,比如迭代一個不算太精確的pi啊,隨機演算法算個概率啊之類的。
關鍵的是要知道你想做什麼,浮點數有他好用的地方,也有他有瓶頸的地方。說出現浮點數或者epsilon一定錯有些矯枉過正了。
如果要深究到底浮點數應該怎麼做才會更正確,就要先去學一下浮點數是如何表示的,IEEE standard的32bit浮點數里有1位sign,8位exponent,23位mantissa,也就是說表示任何一個數的精度大概是1/2^23。換言之,你舉的例子很對,對於不同大小的數用同樣的epsilon是不對的。而你提到的1e-6,簡單解釋一下就是,FLT_EPSILON大概是1.19e-7,也就是1/2^23,也就是浮點數可以表達的最小的使得1+FLT_EPSILON != 1的數。而1e-6大概比這個多一個數量級,也就是大約3個bit。就是針對1附近的運算,允許3個bit的誤差。很多人提到了做一個除法求相對值,就是把結果拉到1附近。
做一點點除法的分析吧,假設兩個數a和b在同一個數量級且a&>=b,誤差分別為a×f,b×f,其中f為FLT_EPSILON。換句話說a, b企圖表達的準確值只被浮點數的表達能力影響。這時候a, b企圖表達的準確值分別是a(1-f)~a(1+f)和b(1-f)~b(1+f)之間的數。兩個數相除我們得到的浮點數是(a/b)(1-f)~(1/b)(1+f),而準確值最大誤差是(a/b)(1+f)/(1-f)~=a/b(1+2f)。可見這個除法導致了兩件事:
1、當a,b在同一個數量級的時候,把誤差帶到了f的數量級,這時候用FLT_EPSILON的數倍就變得更有意義,比如1e-6。
2、除法本身導致了誤差增加,結果誤差不再僅僅是表達誤差,而引入了計算誤差。換句話說兩個不準的數,除了一下變得更不準了。而正是這種浮點數的累積誤差使得我們在需要非常嚴密邏輯的時候,對浮點數敬而遠之。
無論是 |a-b| &< epsilon,還是 |a/b-1.0| &< epsilon,都不能放之四海而皆準!
寫代碼的人應該都知道,比較兩個浮點數是否相等一般是用 if(abs(f1-f2)&
這句話太傷我感情了^_^,因為我一直認為,代碼中出現epsilon的,十有八九是錯誤的。
要想和實數結果精確一致,就不能用浮點數;可用浮點數的場合下,不會要求和實數結果精確一致,即永遠用不到什麼epsilon。(當然,以上是一般情況,「黑魔法」我也搞過)
對於表達式1,設a是浮點運算後的結果,b是實數運算後的結果,其差值為0.1
對於表達式2,設c是浮點運算後的結果,d是實數運算後的結果,其差值為0.2
對於不同的表達式,浮點運算結果和實數運算結果 的差值 可能不一樣,這一點沒疑問吧,所以我舉一個0.1和0.2的例子。
假設epsilon取0.1,表達式2中應該相等的被判為不等了,錯!
假設epsilon取0.2,表達式1中應該不等的被判為相等了,錯!
假設epsilon取0.15,表達式1中應該不等的被判為相等了,表達式2中應該相等的被判為不等了,同時錯!
總之epsilon取什麼值都會帶來誤判。
以前找過相關資料:
為什麼C語言中浮點數(double/float)沒辦法進行等於關係邏輯運算? - myd7349 的回答
其中,這幾篇文章探討了若干浮點數比較的演算法:
Floating-point comparison algorithms
http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm
The UTF testing tools reference
----
當絕對值比較大時可以考慮比較相對誤差
程序猿都不學數值分析的么...
#include &
#include &
float f1, f2;
/* ...... */
if (fabs(f1 - f2) / f1 &< FLT_EPSILON) {
/* f1 ≈ f2 */
} else {
/* f1 != f2 */
}
浮點數,至少在國內,目前絕大多數情況下是處於被誤解和濫用的狀態。
不要講誤差,這個太高深,對於很多人來說根本沒概念,
就連浮點數本身是什麼,
以及什麼情況下應該用浮點數,什麼情況下不該用浮點數,
對很多人(包括不少職業程序員)來說都是混沌不清的。
所以難怪會有北京普林斯頓大學化學專業的道德博士後跳出來對編程發表
比如27/2 + 63/6這樣的情形,用整形變數是搞不定的
這樣的奇談怪論,居然會得到不少贊同。
馮諾依曼最初是極力反對浮點數這種發明的,
因為他一開始預料到了這種「有缺陷的抽象」一定會被很多無知的人濫用以至於被用爛,
幾十年後,天朝濫用浮點數的狀況比馮諾依曼能夠想像到的還要糟糕的多。
實際上一切用浮點數能解決的問題都可以用整數類型解決,
那些離開浮點數就玩不轉計算,
或使用浮點數而根本不考慮誤差,
甚至於無恥地要求反對者給出反例否則就說明自己使用浮點數不用做誤差分析因而就是使用得正確的傢伙們,
在我看來,根本就沒資格侈談「編程」這兩個字。
可以看看業界大佬谷歌是如何處理的:(Github: google/googletest)
使用例子:
double left = // something
double right = // something
const FloatingPoint&
if (lhs.AlmostEquals(rhs)) {
//they"re equal!
}
主要方法:
在bits可能被丟掉前記錄下來,並最後結合階數綜合考慮
// Copyright 2005, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Authors: wan@google.com (Zhanyong Wan), eefacm@gmail.com (Sean Mcafee)
//
// The Google C++ Testing Framework (Google Test)
// This template class serves as a compile-time function from size to
// type. It maps a size in bytes to a primitive type with that
// size. e.g.
//
// TypeWithSize&<4&>::UInt
//
// is typedef-ed to be unsigned int (unsigned integer made up of 4
// bytes).
//
// Such functionality should belong to STL, but I cannot find it
// there.
//
// Google Test uses this class in the implementation of floating-point
// comparison.
//
// For now it only handles UInt (unsigned int) as that"s all Google Test
// needs. Other types can be easily added in the future if need
// arises.
template &
class TypeWithSize {
public:
// This prevents the user from using TypeWithSize&
// values of N.
typedef void UInt;
};
// The specialization for size 4.
template &<&>
class TypeWithSize&<4&> {
public:
// unsigned int has size 4 in both gcc and MSVC.
//
// As base/basictypes.h doesn"t compile on Windows, we cannot use
// uint32, uint64, and etc here.
typedef int Int;
typedef unsigned int UInt;
};
// The specialization for size 8.
template &<&>
class TypeWithSize&<8&> {
public:
#if GTEST_OS_WINDOWS
typedef __int64 Int;
typedef unsigned __int64 UInt;
#else
typedef long long Int; // NOLINT
typedef unsigned long long UInt; // NOLINT
#endif // GTEST_OS_WINDOWS
};
// This template class represents an IEEE floating-point number
// (either single-precision or double-precision, depending on the
// template parameters).
//
// The purpose of this class is to do more sophisticated number
// comparison. (Due to round-off error, etc, it"s very unlikely that
// two floating-points will be equal exactly. Hence a naive
// comparison by the == operation often doesn"t work.)
//
// Format of IEEE floating-point:
//
// The most-significant bit being the leftmost, an IEEE
// floating-point looks like
//
// sign_bit exponent_bits fraction_bits
//
// Here, sign_bit is a single bit that designates the sign of the
// number.
//
// For float, there are 8 exponent bits and 23 fraction bits.
//
// For double, there are 11 exponent bits and 52 fraction bits.
//
// More details can be found at
// http://en.wikipedia.org/wiki/IEEE_floating-point_standard.
//
// Template parameter:
//
// RawType: the raw floating-point type (either float or double)
template &
class FloatingPoint {
public:
// Defines the unsigned integer type that has the same size as the
// floating point number.
typedef typename TypeWithSize&
// Constants.
// # of bits in a number.
static const size_t kBitCount = 8*sizeof(RawType);
// # of fraction bits in a number.
static const size_t kFractionBitCount =
std::numeric_limits&
// # of exponent bits in a number.
static const size_t kExponentBitCount = kBitCount - 1 - kFractionBitCount;
// The mask for the sign bit.
static const Bits kSignBitMask = static_cast&
// The mask for the exponent bits.
static const Bits kExponentBitMask = ~(kSignBitMask | kFractionBitMask);
// How many ULP"s (Units in the Last Place) we want to tolerate when
// comparing two numbers. The larger the value, the more error we
// allow. A 0 value means that two numbers must be exactly the same
// to be considered equal.
//
// The maximum error of a single floating-point operation is 0.5
// units in the last place. On Intel CPU"s, all floating-point
// calculations are done with 80-bit precision, while double has 64
// bits. Therefore, 4 should be enough for ordinary use.
//
// See the following article for more details on ULP:
// http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm.
static const size_t kMaxUlps = 4;
// Constructs a FloatingPoint from a raw floating-point number.
//
// On an Intel CPU, passing a non-normalized NAN (Not a Number)
// around may change its bits, although the new value is guaranteed
// to be also a NAN. Therefore, don"t expect this constructor to
// preserve the bits in x when x is a NAN.
explicit FloatingPoint(const RawType x) { u_.value_ = x; }
// Static methods
// Reinterprets a bit pattern as a floating-point number.
//
// This function is needed to test the AlmostEquals() method.
static RawType ReinterpretBits(const Bits bits) {
FloatingPoint fp(0);
fp.u_.bits_ = bits;
return fp.u_.value_;
}
// Returns the floating-point number that represent positive infinity.
static RawType Infinity() {
return ReinterpretBits(kExponentBitMask);
}
// Non-static methods
// Returns the bits that represents this number.
const Bits bits() const { return u_.bits_; }
// Returns the exponent bits of this number.
Bits exponent_bits() const { return kExponentBitMask u_.bits_; }
// Returns the fraction bits of this number.
Bits fraction_bits() const { return kFractionBitMask u_.bits_; }
// Returns the sign bit of this number.
Bits sign_bit() const { return kSignBitMask u_.bits_; }
// Returns true iff this is NAN (not a number).
bool is_nan() const {
// It"s a NAN if the exponent bits are all ones and the fraction
// bits are not entirely zeros.
return (exponent_bits() == kExponentBitMask) (fraction_bits() != 0);
}
// Returns true iff this number is at most kMaxUlps ULP"s away from
// rhs. In particular, this function:
//
// - returns false if either number is (or both are) NAN.
// - treats really large numbers as almost equal to infinity.
// - thinks +0.0 and -0.0 are 0 DLP"s apart.
bool AlmostEquals(const FloatingPoint rhs) const {
// The IEEE standard says that any comparison operation involving
// a NAN must return false.
if (is_nan() || rhs.is_nan()) return false;
return DistanceBetweenSignAndMagnitudeNumbers(u_.bits_, rhs.u_.bits_)
&<= kMaxUlps;
}
private:
// The data type used to store the actual floating-point number.
union FloatingPointUnion {
RawType value_; // The raw floating-point number.
Bits bits_; // The bits that represent the number.
};
// Converts an integer from the sign-and-magnitude representation to
// the biased representation. More precisely, let N be 2 to the
// power of (kBitCount - 1), an integer x is represented by the
// unsigned number x + N.
//
// For instance,
//
// -N + 1 (the most negative number representable using
// sign-and-magnitude) is represented by 1;
// 0 is represented by N; and
// N - 1 (the biggest number representable using
// sign-and-magnitude) is represented by 2N - 1.
//
// Read http://en.wikipedia.org/wiki/Signed_number_representations
// for more details on signed number representations.
static Bits SignAndMagnitudeToBiased(const Bits sam) {
if (kSignBitMask sam) {
// sam represents a negative number.
return ~sam + 1;
} else {
// sam represents a positive number.
return kSignBitMask | sam;
}
}
// Given two numbers in the sign-and-magnitude representation,
// returns the distance between them as an unsigned number.
static Bits DistanceBetweenSignAndMagnitudeNumbers(const Bits sam1,
const Bits sam2) {
const Bits biased1 = SignAndMagnitudeToBiased(sam1);
const Bits biased2 = SignAndMagnitudeToBiased(sam2);
return (biased1 &>= biased2) ? (biased1 - biased2) : (biased2 - biased1);
}
FloatingPointUnion u_;
};
boost有一個有理數類,boost::rational,可以不損失精度地進行有理數運算。當然,用來做大量圖形學計算是不現實的,會產生超級大的整型的分子和分母。可能有的情景下,需要用到的是有理數類而不是浮點數類。
abs(x-y)/abs(x) &< 1e-6
可能好一點?
推薦閱讀: