National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 05

浮點數:為什麼 0.1 + 0.2 不等於 0.3

IEEE 754 的符號、指數與尾數
Interaction 25 minute(s)

Learning Objectives

  • 說明十進位小數換成二進位時,為什麼有些數(例如 0.1)寫不完
  • 看懂 IEEE 754 浮點數的三個欄位:符號、指數(含偏移值)、尾數(含隱藏位元)
  • 認識特殊值:正負零、無限大、NaN 與次正規數
  • 解釋 0.1 + 0.2 != 0.3 的原因,寫程式時用容許誤差、整數或 Decimal 處理小數

0.1 + 0.2 是多少

在 Python 裡輸入 0.1 + 0.2,得到的不是 0.3:

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

這不是 Python 的錯,JavaScript、Java、C、試算表軟體在底層都會遇到同樣的事;只是有些軟體顯示時先四捨五入,你看不出來。

原因在於電腦用二進位存小數,而且每個數只有固定的位元數(常見的是 32 或 64 個位元)。這一課要回答三個問題:

  1. 十進位的 0.1 換成二進位,為什麼寫不完?
  2. 電腦用什麼格式存小數?這個格式叫做浮點數(floating-point number),現在幾乎所有電腦都照 IEEE 754 標準。
  3. 寫程式處理小數(尤其是金額)時要注意什麼?

二進位的小數

十進位小數點後每一位的權重是 1/10、1/100、1/1000…;二進位小數點後的權重則是 1/2、1/4、1/8、1/16…:

二進位 計算 十進位
0.1₂ 1/2 0.5
0.01₂ 1/4 0.25
0.101₂ 1/2 + 1/8 0.625
0.11₂ 1/2 + 1/4 0.75

反過來,要把十進位小數換成二進位,可以一直乘以 2、取整數部分:0.625 × 2 = 1.25(取 1),0.25 × 2 = 0.5(取 0),0.5 × 2 = 1.0(取 1),小數部分變成 0 就結束,所以 0.625 = 0.101₂。

十進位的 1/3 寫成 0.3333… 永遠寫不完,因為 3 不是 10 的因數。二進位也一樣:只有分母是 2 的次方的分數(1/2、3/4、5/8…)能用有限位數寫完。0.1 = 1/10,分母有因數 5,所以換成二進位會無限循環:

0.1 = 0.0001100110011001100110011…₂(0011 一直重複)

電腦的位元數有限,只能存到某一位就截斷(其實是捨入到最接近的值),所以存進去的 0.1 本來就不是剛好 0.1。

一直乘以 2:把 0.625 與 0.1 換成二進位

為了讓計算完全精確,程式用分數表示小數:num / den(0.625 是 5/8、0.1 是 1/10)。每一圈把小數乘以 2,整數部分就是下一位二進位數字。注意上方格子裡 bits 怎麼一位一位長出來:0.625 三圈就結束,0.1 算滿 12 位還停不下來。

IEEE 754:符號、指數、尾數

十進位的科學記號把 6,020,000 寫成 6.02 × 10⁶。浮點數用的是二進位的科學記號:

值 = (±) 1.xxxxx₂ × 2^E

小數點的位置由指數決定,可以「浮動」,所以叫浮點數。IEEE 754 把一個數分成三個欄位:

欄位 32 位元(單精度) 64 位元(雙精度) 存什麼
符號(sign) 1 位元 1 位元 0 是正、1 是負
指數(exponent) 8 位元 11 位元 E 加上偏移值(32 位元是 127,64 位元是 1023)之後存成無號整數,這樣負的指數也不需要另外的符號
尾數(fraction) 23 位元 52 位元 1.xxxxx 小數點後面的 xxxxx;最前面的 1 一定存在,所以不用存(隱藏位元),等於多賺了一個位元的精度

例如 3.0 = 1.1₂ × 2¹:符號 0;指數 1 + 127 = 128 = 10000000;尾數 1000…0(1.1 小數點後面的 1)。

常見語言的對應:Python 的 float 與 JavaScript 的數字都是 64 位元;C、Java 的 float 是 32 位元、double 是 64 位元。

IEEE 754 的第一版在 1985 年發布;2008 年的修訂版加入了十進位浮點數與 16 位元的半精度等格式;2019 年再做一次小幅修訂。

互動練習:點位元組出一個浮點數

上面是 32 位元浮點數的 32 個位元:紅色是符號、藍色是指數、綠色是尾數,點一下就在 0 與 1 之間切換,下方會算出指數減掉偏移值、尾數加上隱藏位元,以及最後存的完整十進位值。依任務先用點位元的方式湊出 1.0 與 -2.5,再在輸入框存入 0.1,看實際存的值與誤差;最後把指數全部點成 1、再全部改回 0,認識無限大、NaN 與次正規數。也可以切到 64 位元看看 0.1 的誤差變小多少。

特殊值、範圍與精度

指數欄位全 0 與全 1 保留給特殊值:

指數欄位 尾數 代表
全 0 全 0 ±0(有正零與負零,兩者相等)
全 0 不是 0 次正規數:小數點前改成 0.,用來表示比最小正規數更接近 0 的數,讓數值慢慢變小到 0,而不是突然跳到 0
全 1 全 0 ±∞(無限大,例如太大的數溢位)
全 1 不是 0 NaN(Not a Number),例如 0/0、∞ − ∞;NaN 和任何數比較(包括自己)都不相等

位元數決定了範圍與精度:

32 位元 64 位元
最大的數 約 3.4 × 10³⁸ 約 1.8 × 10³⁰⁸
最小的正規正數 約 1.2 × 10⁻³⁸ 約 2.2 × 10⁻³⁰⁸
有效位數(含隱藏位元) 24 個二進位位數,大約 7 位十進位數 53 個二進位位數,大約 15~16 位十進位數

精度有限的另一個結果:數字很大時,相鄰兩個能表示的數之間差距也很大。64 位元浮點數在 2⁵³ 之後就連相鄰的整數都分不開了。

想一想:看位元算數值

1.

一個 32 位元浮點數:符號 0、指數欄位 10000001(129)、尾數全 0。它的值是多少?

2.

下列哪一個十進位數,可以被二進位浮點數剛好存下來,沒有誤差?

3.

指數欄位全部是 1、尾數不是 0,這個浮點數代表什麼?

第 1 次作答

在程式裡比較小數

看 Python 怎麼印出 0.1 + 0.2,以及幾種比較小數的寫法。f'{0.1:.20f}' 讓 Python 印出小數點後 20 位,可以看到 0.1 實際存的值。最後兩種寫法完全避開二進位小數的誤差。

互動練習:0.1 + 0.2 與 0.3

輸入 A、B、C 後按「計算」,表格會列出每個數實際存的值(完整十進位)與位元樣式,以及兩個存的值相加後、再捨入回浮點數的結果。依任務先用 64 位元算 0.1 + 0.2 和 0.3 比較,再切到 32 位元算一次並回答問題;接著找一組不是整數、但加起來剛好相等的數;最後用下面的「連加」把 0.1 連加 10 次。

寫程式處理小數的建議

情況 建議
比較兩個浮點數 不要用 ==,改用 math.isclose(a, b) 或 abs(a - b) < 容許誤差
金額、點數這類需要「剛好」的數 用整數存最小單位(分、角),或用 decimal.Decimal;資料庫有 DECIMAL 型別
大量累加 誤差會一點一點累積;可以用 math.fsum() 這類專門的加總方法
顯示給使用者看 f'{x:.2f}' 只改變顯示的位數,存的值不變

浮點數不是「不準」,而是用固定的位元數換取很大的範圍與很快的計算速度。科學計算、繪圖、機器學習都大量使用浮點數;只要知道誤差從哪裡來,就能選對工具。

想一想:程式會印出什麼

1.

下面的程式會印出什麼?

print(0.5 + 0.25 == 0.75)
2.

下面的程式會印出什麼?

total = 0
for i in range(10):
    total += 0.1
print(total == 1.0)
3.

下面的程式會印出什麼?(2⁵³ = 9007199254740992)

print(float(2**53 + 1) == 2**53)
第 1 次作答

重點整理

  • 二進位小數的位權是 1/2、1/4、1/8…;只有分母是 2 的次方的分數能用有限位數寫完,0.1、0.2、0.3 都會無限循環。
  • IEEE 754 浮點數 =(符號)1.尾數₂ × 2^(指數欄位 − 偏移值);32 位元是 1/8/23 位元、偏移 127,64 位元是 1/11/52 位元、偏移 1023。
  • 尾數最前面的 1 不用存(隱藏位元);指數全 0、全 1 保留給 ±0、次正規數、±∞ 與 NaN。
  • 0.1 + 0.2 != 0.3:兩個數本來就存得不準,相加後再捨入一次,和「最接近 0.3 的浮點數」差了一個最小刻度。
  • 比較浮點數用 math.isclose() 或容許誤差;金額用整數或 Decimal。

下一課:數字講完了,換文字。電腦怎麼把「中」這個字存成位元組?為什麼打開檔案有時候會看到「中文」這種亂碼?

想親手寫更多 Python,可以接著上 Python 軌。

這一課完成了!
Next lesson:文字編碼:ASCII、Unicode 與 UTF-8
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。