浮点数(Floating-point)是计算机表示带小数的实数的方式,核心思想是二进制的科学计数法——让小数点位置可以"浮动"。
与之对应的两种表示方式:
| 类型 | 英文 | 小数点位置 | 示例 |
|---|---|---|---|
| 整数 | Integer | 无小数点 | 00000101 = 5 |
| 定点数 | Fixed-point | 固定在某位后 | 00101.0100 = 5.25 |
| 浮点数 | Floating-point | 随指数浮动 | 1.0101 × 2² = 5.25 |
定点数的小数点位置由硬件或软件约定,始终不变;浮点数则通过指数调整小数点位置,能用同样的位数表示极大或极小的数。
几乎所有现代计算机的浮点运算都遵循 IEEE 754(IEEE Standard for Floating-Point Arithmetic),1985 年首次发布,由伯克利分校 William Kahan 教授主导设计(1989 年图灵奖)。
一个浮点数被拆分为三部分:
(-1)^符号位 × 1.尾数 × 2^(指数 - 偏移量)
float32: [1位符号] [8位指数] [23位尾数] 共 32 位
float64: [1位符号] [11位指数] [52位尾数] 共 64 位
所谓「32 位」就是 32 个二进制位(bit),每个位只能是 0 或 1。各类型的位分配如下:
float16: 0 | 00000 | 0000000000 共 16 位
5位指数 10位尾数
bfloat16: 0 | 00000000 | 0000000 共 16 位
8位指数 7位尾数
float32: 0 | 00000000 | 00000000000000000000000 共 32 位
8位指数 23位尾数
float64: 0 | 00000000000 | 0000000000...000 共 64 位
11位指数 52位尾数
以 3.14 为例,float32 在内存中的 32 位二进制为:
二进制: 0 10000000 10010001111010111000011
│ ──指数── ────── 尾数 ──────────────
符号
逐步还原为十进制:
10000000₂ = 128,实际指数 = 128 - 127(偏移量)= 110010001111010111000011,加上隐含的前导 1 → 1.10010001111010111000011值 = (-1)^0 × 1.10010001111010111000011₂ × 2^1
= 1 × 1.10010001111010111000011₂ × 2
= 11.0010001111010111000011₂ (小数点右移 1 位)
= 3 + 0.1400001... (二进制转十进制)
≈ 3.14
偏移量(bias) 的作用:float32 的指数范围是 0~255,减去 127 后实际指数为 -127~+128,这样既能表示极大数也能表示极小数。其中 0 和 255 被保留用于特殊值(零、无穷大、NaN)。
| 类型 | 总位数 | 符号 | 指数 | 尾数 | 有效精度 | 范围 |
|---|---|---|---|---|---|---|
| float16(半精度) | 16 | 1 | 5 | 10 | ~3 位 | ±65504 |
| bfloat16 | 16 | 1 | 8 | 7 | ~3 位 | ±3.4×10³⁸ |
| float32(单精度) | 32 | 1 | 8 | 23 | ~7 位 | ±3.4×10³⁸ |
| float64(双精度) | 64 | 1 | 11 | 52 | ~15 位 | ±1.8×10³⁰⁸ |
bfloat16 vs float16:bfloat16(Brain Floating Point)由 Google 于 2016 年为 TPU 设计,不是 IEEE 754 标准的一部分,但已成为 AI 训练的事实标准(Intel、NVIDIA、ARM 等硬件均支持)。它的指数位更多(8 vs 5),范围与 float32 相同,但精度更低。
为什么没有 bfloat32? bfloat16 的设计初衷就是做一个「和 float32 范围一样大、但只有 16 位的缩水版」——它保留了 float32 的 8 位指数,只把尾数从 23 位砍到 7 位。如果搞一个 bfloat32,那它就等于 float32 本身,没有意义。所以
bf前缀只用在 16 位上。
IEEE 754 还定义了几个特殊值:
float('inf') # 正无穷
float('-inf') # 负无穷
float('nan') # NaN(Not a Number),如 0/0 的结果
import math
math.isinf(float('inf')) # True
math.isnan(float('nan')) # True
IEEE 754 用二进制表示小数,但很多十进制小数在二进制下是无限循环的:
十进制 0.1 = 二进制 0.00011001100110011...(无限循环)
十进制 0.2 = 二进制 0.00110011001100110...(无限循环)
就像十进制无法精确写出 1/3 = 0.3333...一样,二进制也无法精确表示 0.1。浮点数只能截断到有限位,产生微小误差。
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
原因链:0.1 和 0.2 各自存储时就有微小截断误差 → 两个近似值相加 → 误差叠加 → 结果不再是精确的 0.3。
这不是 Python 的 bug,而是所有使用 IEEE 754 的语言(C/C++/Java/JavaScript/Go/Rust……)的共同行为。
# 用 mpmath 查看 float(0.1) 的真实存储值
from mpmath import mp
mp.dps = 60
mp.mpf(0.1) # 注意:传入的是 float,不是字符串
# 结果:0.1000000000000000055511151231257827021181583404541015625
# 而不是精确的 0.1
浮点运算中,当两个数差距很大时,小数可能被"吞掉":
>>> 1e16 + 1.0
1e+16 # 1.0 被吞掉了!
>>> 1e16 + 1.0 == 1e16
True # 1.0 完全丢失
这是因为 float64 只有 ~15 位有效数字,10000000000000001 需要 17 位才能精确表示,超出了精度上限。
import struct
# float32 的二进制
data = struct.pack('>f', 3.14) # '>f' = 大端序 float
print(' '.join(f'{b:08b}' for b in data))
# 01000000 01001000 11110101 11000011
# │符号 │── 指数 ──│──────── 尾数 ────────│
# float64 的二进制
data = struct.pack('>d', 3.14) # '>d' = 大端序 double
print(' '.join(f'{b:08b}' for b in data))
import struct, math
from mpmath import mp
mp.dps = 50
pi_true = mp.pi
# float32
f32 = struct.unpack('f', struct.pack('f', math.pi))[0]
print(f"float32: {f32:.20f}")
# float64
print(f"float64: {math.pi:.20f}")
# 真值
print(f"真值: {pi_true}")
# 误差
print(f"f32 误差: {abs(pi_true - mp.mpf(f32))}")
print(f"f64 误差: {abs(pi_true - mp.mpf(math.pi))}")
# decimal — 十进制精确运算
from decimal import Decimal, getcontext
Decimal('0.1') + Decimal('0.2') # Decimal('0.3')
# mpmath — 任意精度数学
from mpmath import mp
mp.dps = 100
mp.pi # π 的 100 位精确值
# fractions — 分数精确运算
from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6) # Fraction(1, 2)
深度学习中的模型量化,本质上就是在不同浮点/定点精度之间做权衡:
训练阶段:float32(高精度,慢)
↓ 混合精度训练
推理阶段:float16 / bfloat16(中精度,快)
↓ 量化
边缘部署:int8 / int4(定点,更快更小)
import numpy as np
# 原始权重(float32)
weights = np.array([0.1, -0.5, 0.3, 1.2], dtype=np.float32)
# 量化:float32 → int8
scale = np.max(np.abs(weights)) / 127
quantized = np.round(weights / scale).astype(np.int8)
# 反量化:int8 → float32(有损失)
dequantized = quantized.astype(np.float32) * scale
# 误差
print(f"原始: {weights}")
print(f"还原: {dequantized}")
print(f"误差: {weights - dequantized}")
神经网络对精度损失有天然的鲁棒性——少量的浮点误差不影响最终的分类/生成结果。量化就是利用这种鲁棒性,用可控的精度损失换取:
浮点运算比定点运算(本质是整数运算)多出多个步骤:
定点加法(直接做整数加法,约定小数点位置):
5.25 + 3.75
= 525 + 375(约定小数点后 2 位)
= 900 → 9.00
一步搞定,CPU 的 ALU(算术逻辑单元)原生支持。
浮点加法(需要 4 步):
1.0101 × 2² + 1.1110 × 2¹
步骤 1:对齐指数 → 1.1110 × 2¹ 变成 0.1111 × 2²
步骤 2:尾数相加 → 1.0101 + 0.1111 = 10.0100
步骤 3:规格化 → 10.0100 × 2² 变成 1.00100 × 2³
步骤 4:舍入 → 截断到有效位数
浮点运算单元(FPU)比整数运算单元(ALU)电路复杂得多,因此在资源受限的硬件上,定点运算更快、更省电。
在现代 x86 CPU(Intel/AMD)上,实测结果往往相反:
import numpy as np
import time
N = 10_000_000
f32_a = np.random.randn(N).astype(np.float32)
f32_b = np.random.randn(N).astype(np.float32)
i32_a = np.random.randint(-1000, 1000, N, dtype=np.int32)
i32_b = np.random.randint(-1000, 1000, N, dtype=np.int32)
# 定点模拟:用整数运算 + 缩放因子
SCALE = 1000
fix_a = (f32_a * SCALE).astype(np.int32)
fix_b = (f32_b * SCALE).astype(np.int32)
def bench(op, a, b, label):
for _ in range(5): op(a, b) # 预热
start = time.perf_counter()
for _ in range(50): op(a, b)
elapsed = time.perf_counter() - start
print(f"{label:16s}: {elapsed:.4f}s")
print(f"数组大小: {N:,} 个元素,50 次运算\n")
bench(np.add, f32_a, f32_b, "float32 加法")
bench(np.add, i32_a, i32_b, "int32 加法")
bench(np.add, fix_a, fix_b, "定点(整数) 加法")
print()
bench(np.multiply, f32_a, f32_b, "float32 乘法")
bench(np.multiply, i32_a, i32_b, "int32 乘法")
bench(lambda a,b: np.multiply(a, b) // SCALE, fix_a, fix_b, "定点(整数) 乘法")
x86 PC 实测结果:
| 运算 | float32 | int32 | 定点(整数) |
|---|---|---|---|
| 加法 | 0.53s | 0.56s | 0.57s |
| 乘法 | 0.53s | 0.63s | 1.17s |
原因:x86 CPU 有极其强大的 FPU + SIMD(SSE/AVX)指令集,numpy 底层高度优化了 float32 的向量化路径。定点乘法还需额外做一步整数除法 // SCALE,反而更慢。
定点运算的优势不在 PC/GPU,而在 NPU、FPGA、微控制器 等资源受限硬件:
以 RK3588(香橙派)的 NPU 为例:
| 单元 | 擅长 | float32 算力 | int8 算力 |
|---|---|---|---|
| CPU(ARM Cortex-A76) | 通用计算 | 中等 | 中等 |
| GPU(Mali-G610) | 图形/浮点 | 较强 | 弱 |
| NPU(6 TOPS) | AI 推理 | 弱 | 极强 |
NPU 内部堆了大量 int8 乘法器(电路简单,一个加法器就够),但只有很少的 float32 运算单元(电路复杂,要对齐指数、规格化)。因此同一个 NPU 上,int8 算力可以是 float32 的 4~8 倍。
这正是模型量化到 int8 在边缘设备上推理更快的根本原因——不是因为「整数天生快」,而是因为边缘硬件上整数运算单元比浮点单元多得多。
| 中文 | 英文 | 含义 |
|---|---|---|
| 浮点数 | Floating-point | 小数点位置可变的数 |
| 定点数 | Fixed-point | 小数点位置固定的数 |
| 整数 | Integer | 没有小数部分的数 |
| 单精度 | Single-precision | float32,32 位 |
| 双精度 | Double-precision | float64,64 位 |
| 半精度 | Half-precision | float16,16 位 |
| 尾数 | Mantissa / Significand | 有效数字部分 |
| 指数 | Exponent | 2 的幂次 |
| 溢出 | Overflow | 超出最大范围 |
| 下溢 | Underflow | 小于最小可表示值 |
| 舍入误差 | Rounding error | 截断导致的精度损失 |
| 大数吃小数 | Catastrophic cancellation | 差距过大的数相加/减时精度丢失 |