Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

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

資料的數位表示

資料的數位表示

從位元與位元組出發,理解整數的二補數、浮點數 IEEE 754 的尾數與指數結構,以及為什麼 0.1 + 0.2 永遠不等於 0.3

為什麼計算機算不出 0.1 + 0.2 = 0.3

打開任何一個程式語言的互動環境,輸入 0.1 + 0.2,你會得到一個令人困惑的答案:

>>> 0.1 + 0.2
0.30000000000000004

這不是程式語言的 bug,也不是你的電腦壞了。世界上幾乎每一台電腦、每一支手機、每一個科學計算器,在這道題目上都會給出同樣「錯誤」的結果。要理解為什麼,我們得回到最根本的問題:當資料進入計算機,它究竟長什麼樣子?文字、整數、小數、圖片、聲音——這些在我們眼中如此不同的東西,最終都要被翻譯成同一種語言:一連串的 0 與 1。這個翻譯的規則,就是「資料的數位表示」。

計算的基礎概念示意圖

一切的起點:位元與位元組

計算機的最小資訊單位是位元(bit),它只有兩個可能的狀態:0 或 1。為什麼是二進位而非我們熟悉的十進位?原因很實際:電子電路最容易、最可靠地分辨的就是「有電壓」與「沒電壓」兩種狀態。用兩個離散狀態來編碼資訊,抗雜訊能力遠勝於試圖區分十個不同的電壓位階。

單一位元能表達的資訊太少,因此我們把 8 個位元綁成一組,稱為一個位元組(byte)。一個位元組有 $2^8 = 256$ 種不同的組合,可以表示 0 到 255 的整數,或對應到一個字元(早期 ASCII 編碼正是如此)。

關鍵的觀念是:位元本身沒有意義,意義來自我們約定的「解讀方式」。 同一串位元 01000001,如果約定它是無號整數,就是 65;如果約定它是 ASCII 字元,就是大寫字母 A;如果約定它是某種旗標集合,又是另一回事。資料表示的核心,其實是「人與機器之間的約定」。

動手看一個例子:二進位與十進位的換算

我們用每個位元代表的「權重」來理解二進位數字。以 8 位元的 01000001 為例:

位元位置:  7   6   5   4   3   2   1   0
位元值:    0   1   0   0   0   0   0   1
權重:     128  64  32  16   8   4   2   1
貢獻:      0  +64 + 0 + 0 + 0 + 0 + 0 +1  = 65

每往左一位,權重就乘以 2。這和十進位每往左一位乘以 10 是同樣的道理,只是底數從 10 換成了 2。一個 $n$ 位元的無號整數,能表示的範圍是 $0$ 到 $2^n - 1$。

整數:有號與無號的取捨

如果我們只需要表示非負整數(如年齡、計數),直接用上述方式就好,稱為無號整數(unsigned integer)。但真實世界有負數:溫度、帳戶餘額、座標差。我們需要一種方式在位元裡塞進「正負號」。

最直覺的想法是拿最高位當符號位(0 為正、1 為負),這叫原碼(sign-magnitude)。但它有兩個惱人的問題:第一,會出現 +0-0 兩個零;第二,加法電路必須先判斷符號再決定加還是減,硬體變複雜。

現代計算機普遍採用二補數(two's complement)。它的規則很簡潔:要表示一個負數 $-x$,就取 $x$ 的位元表示後「逐位取反再加 1」。它的精妙之處在於:

  • 零只有一種表示(沒有 -0 的困擾)。
  • 加減法可以用同一套電路完成,不需要特別處理符號。
  • 最高位仍可視為符號位,但它的權重是「負的」。

動手看一個例子:用二補數表示 −5

以 8 位元為例,先寫出 5 的二進位,再做二補數運算:

  5 的二進位:    0000 0101
  逐位取反:      1111 1010
  加 1:          1111 1011   ← 這就是 -5

驗證一下:在二補數中,最高位的權重是 $-128$,其餘照常。

$$-128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -5$$

確實是 $-5$。一個 $n$ 位元的二補數整數,表示範圍是 $-2^{n-1}$ 到 $2^{n-1} - 1$。注意這個範圍不對稱:負數比正數多一個,因為零佔用了正數那側的一個位置。這也是為什麼 8 位元有號整數的範圍是 $-128$ 到 $+127$,而不是 $-127$ 到 $+127$。

理解二補數還能幫你避開一個常見陷阱:整數溢位(overflow)。當運算結果超出可表示範圍時,數值會「繞回」。例如 8 位元有號整數的 $127 + 1$,在位元層面會變成 1000 0000,也就是 $-128$。許多嚴重的軟體錯誤——從遊戲分數突然變負,到航太系統的災難——都源自未被察覺的整數溢位。

浮點數:在有限位元裡裝下實數

整數很整齊,但世界充滿小數。我們需要一種方法表示 $3.14159$、$6.022 \times 10^{23}$、$0.0000001$ 這類數字。最自然的構想來自科學記號:把任何數寫成「尾數 × 底數的指數次方」。

這正是浮點數(floating-point) 的核心思想——之所以叫「浮點」,是因為小數點的位置不固定,而是由指數來決定,像是浮動的。業界統一遵循 IEEE 754 標準。以最常用的雙精度(double, 64 位元)為例,64 個位元被切成三段:

欄位 位元數 作用
符號位 sign 1 0 為正、1 為負
指數 exponent 11 決定數值的量級(大小)
尾數 mantissa 52 決定數值的精度(有效數字)

一個正規化的浮點數,其值大致為:

$$(-1)^{\text{sign}} \times 1.\text{mantissa} \times 2^{\text{exponent} - 1023}$$

這裡有兩個值得細看的設計。其一,尾數前面那個 1. 是「隱含位元」——既然正規化後最高位一定是 1,標準就乾脆不存它,省下一個位元換取額外一位精度。其二,指數採「偏移(bias)」表示而非二補數,雙精度的偏移量是 1023,這讓浮點數的位元表示在比大小時可以近似當作整數來比,硬體更好做。

為什麼 0.1 在計算機裡不精確

現在我們可以回答開頭的謎題了。問題的根源在於:二進位的「有限小數」和十進位的「有限小數」並不是同一群數字。

在十進位裡,$0.1$ 是個乾淨俐落的有限小數。但當我們試圖把它寫成二進位的 $\frac{a}{2^k}$ 形式時,會發現辦不到——$0.1$ 在二進位下是個無窮循環小數:

$$0.1_{10} = 0.0001100110011001100110011\ldots_2$$

就像 $\frac{1}{3}$ 在十進位下是無窮的 $0.3333\ldots$ 一樣,$0.1$ 在二進位下也永遠除不盡。由於尾數只有 52 個位元,計算機只能存下這個無窮循環的前一截,然後四捨五入。於是存進去的根本不是 $0.1$,而是一個極度接近、卻略有偏差的數字。

動手看一個例子:看穿被截斷的 0.1

我們可以請 Python 把它「真正存的值」用更多位數印出來:

>>> format(0.1, '.20f')
'0.10000000000000000555'
>>> format(0.2, '.20f')
'0.20000000000000001110'
>>> format(0.3, '.20f')
'0.29999999999999998890'

看出來了嗎?存進去的 0.1 其實是 $0.1000\ldots0555$,比真正的 $0.1$ 大了一點點。當你把這個「略大的 0.1」和「略大的 0.2」相加,誤差累積,結果就成了 $0.30000000000000004$,而它又恰好不等於計算機存的那個「略小的 0.3」。三個微小的捨入誤差湊在一起,造就了那個著名的尾巴。

這給我們一條重要的實務守則:永遠不要用 == 直接比較兩個浮點數是否相等。 正確做法是判斷兩者的差距是否小於一個可接受的微小容差(epsilon):

def almost_equal(a, b, eps=1e-9):
    return abs(a - b) < eps

almost_equal(0.1 + 0.2, 0.3)   # True

另外,凡是涉及金錢的計算,務必避免用浮點數,改用十進位定點數型別(如 Python 的 decimal.Decimal)或直接以「分」為單位的整數運算,以免微小誤差在大量交易中累積成真金白銀的差錯。

重點回顧

  • 位元是最小單位,意義來自約定:同一串 0 與 1,可以是整數、字元或旗標,端看我們如何解讀。8 個位元組成一個位元組。
  • 二補數是現代整數表示的主流:它讓零唯一、加減法共用電路,代價是正負範圍不對稱(如 8 位元為 $-128$ 到 $+127$),並需提防整數溢位繞回。
  • 浮點數借用科學記號的構想:IEEE 754 把數字拆成符號、指數、尾數三段;指數管量級,尾數管精度,並用隱含位元多賺一位精度。
  • 0.1 不精確的本質是進制問題:$0.1$ 在二進位下是無窮循環小數,有限的尾數只能存近似值,捨入誤差累積後造成 $0.1 + 0.2 \neq 0.3$。
  • 實務守則:浮點數比較用容差而非 ==;金錢計算改用定點數或整數,避免捨入誤差釀成實質損失。

深入探討(研究所視角)

要真正掌握浮點數的精度行為,必須深入 IEEE 754 尾數與指數的交互結構,並引入「機器精度」這個量化工具。

尾數與指數如何共同決定可表示的數。雙精度的尾數有 52 個顯式位元加 1 個隱含位元,共 53 位有效位元。這意味著相鄰兩個可表示浮點數之間的間距,並非固定,而是隨數值量級放大。我們定義機器精度(machine epsilon)為 1 與「大於 1 的下一個可表示數」之間的差距:

$$\varepsilon_{\text{machine}} = 2^{-52} \approx 2.22 \times 10^{-16}$$

對於量級在 $2^e$ 附近的數,相鄰浮點數的間距(稱為 ULP, unit in the last place)約為 $2^{e-52}$。這就是浮點數的精妙與危險所在:它在小數值處密集、大數值處稀疏,能用固定位元覆蓋極寬廣的動態範圍(雙精度約 $10^{-308}$ 到 $10^{308}$),代價是相對精度恆定、絕對精度浮動。

捨入誤差的傳播與災難性抵銷。任何一次浮點運算的相對誤差被界定在 $\frac{1}{2}\varepsilon_{\text{machine}}$ 以內(在「round to nearest, ties to even」模式下),這是 IEEE 754 提供的正確捨入保證。然而誤差會在連續運算中累積。最值得警惕的是災難性抵銷(catastrophic cancellation):當兩個非常接近的數相減,高位有效數字互相抵消,結果的有效位元數驟降,原本被埋在尾數末端的捨入誤差被放大到主導地位。例如求解二次方程式 $ax^2+bx+c=0$ 時,若 $b^2 \gg 4ac$,公式 $\frac{-b+\sqrt{b^2-4ac}}{2a}$ 在 $b>0$ 那一根會發生抵銷,數值分析上需改用有理化後的等價公式來規避。這正是數值分析(numerical analysis)這門學科的核心關懷之一。

特殊值與漸進下溢。IEEE 754 還保留指數欄位的全 0 與全 1 來編碼特殊狀態:指數全 1 配尾數 0 表示無窮大($\pm\infty$),配非零尾數表示 NaN(Not a Number,如 $0/0$ 的結果);指數全 0 則進入非正規化(denormalized/subnormal) 模式,去掉隱含的前導 1,讓浮點數在接近零時能「漸進下溢(gradual underflow)」,填補 0 與最小正規數之間的縫隙,避免精度在零附近突然崩塌。這些設計使浮點運算具備可預測的封閉性——任何運算都有定義良好的結果,不會讓程式陷入未定義行為。

與其他主題的連結。資料表示是計算機科學的地基,它向上連結了多個領域:在計算機結構中,浮點運算單元(FPU)的設計與這套標準緊密耦合;在機器學習中,為了在 GPU 上加速訓練,業界發展出半精度(FP16)乃至 bfloat16、FP8 等更短的格式,刻意犧牲尾數精度換取吞吐量與記憶體頻寬,這背後正是對「指數管範圍、尾數管精度」這條原理的策略性運用——bfloat16 保留與 FP32 相同的 8 位元指數(維持動態範圍以免梯度下溢),卻把尾數砍到 7 位元。理解了本文的尾數/指數結構,你才能讀懂為什麼深度學習工程師願意接受更粗糙的精度,以及他們在何處必須小心捨入誤差的累積。資料如何被表示,從來不只是底層細節,而是貫穿整個計算世界的設計哲學。

AI 共讀助教正在陪你讀:資料的數位表示
嗨!我是這篇文章的共讀助教,只根據〈資料的數位表示〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。