计算机如何存储浮点数

对不起,此内容只适用于美式英文

本文讲解符号数存储格式,能表示最大的值是多少,在什么情况下会出现精度损失。

1. 问题描述

遇到一个问题。我使用pd.read_csv读取股票数据,取出交易日期,并且按从旧到新来排序。

csv_file = r'data/Pingan.csv'
data = pd.read_csv(csv_file)

target_df = data.loc[:, ['trade_date']] 
target_df = target_df.iloc[::-1].copy() # reverse trade date from past to present

target_df的值,第一个值为20141117:

trade_date [1592    20141117] [1591    20141118] [1590    20141119] [1589    20141120] [1588    20141121] [...          ...] [4       20210526] [3       20210527] [2       20210528] [1       20210531] [0       20210601] [] [[1593 rows x 1 columns]]

target_df转换成PyTorch的张量,

os.environ['CUDA_VISIBLE_DEVICES'] = '0'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
torch_float_type = torch.float32

stock_array = torch.tensor(target_df.to_numpy(), dtype=torch_float_type).to(device)

奇怪的是,为什么stock_array的值会变成下面这样,第一值为20141116。

tensor([[20141116.],
        [20141118.],
        [20141120.],
        ...,
        [20210528.],
        [20210532.],
        [20210600.]], device='cuda:0')

原因在于torch_float_type = torch.float32,将日期存储为单精度浮点数,精度损失导致。

2. 解释原因

计算机存储浮点数采用IEEE-754 标准,分为单精度浮点数float(32位)和双精度浮点数double(64位)。

2.1 单精度浮点数float

单精度存储格式如下:

s,符号位(1个比特)     |       e,指数位(8个比特)     |           f,有效数位(23个比特)

表示的数值为:$(-1)^s \times 1.f \times 2^{e-127}$,其中,

  • 符号位s,二进制,0表示正数,1表示负数
  • $1.f$,二进制
  • $2^{e-127}$,指数位8位,可以表示的数值范围0~255,e=0(用于表示0或者0.f)和e=255(用于表示无穷大、无穷小、NAN)有特殊的用途,因此e-127实际可以表示的数值范围是-126~127。$2^{e-127}$表示小数点向左(e-127小于0)或向右(e-127大于0)移动e-127位。

举个例子,十进制0.3的二进制表示为0.01001100110011001100110011001100110011001100110011...1001无限循环下去),向左移动两位,即$2^{-2}$(由$2^{e-127}$可得e应为125,对应的二进制为1111101),得到01.001100110011001100110011001100110011001100110011...。但还有一个问题,有效位数最多只能是23位,有两种取法:

  • 超出的部分直接截断,取前23位,前23位00110011001100110011001
  • 考虑舍入,第24位为1,舍入,前23位为00110011001100110011010
001100110011001100110011001100110011001100110011...     # 原数值
00110011001100110011001     # 直接截断
00110011001100110011010     # 舍入

那究竟取哪个值呢。IEEE-754标准给出了舍入的四种方式,默认是向最接近的方向舍入(round to near)。显然,直接截断和舍入两个值中,舍入的值更接近原来的值(可以这么直观理解:第24位是1,十进制为0.5,第24位以后还有一些1,意味着大于0.5,更接近于1,而不是0)。那么,0.3的浮点数在计算机存储为:

0   01111101    00110011001100110011010

符号位:0
指数位:01111101
有效数位:00110011001100110011010

这里面还有一个小问题,如果有效位数恰好是24位,第24位恰好为1(直观理解成0.5,0.5与0和1的距离是一样的),这就要用到一个补充规则,偶数优先(round-to-even),即让最低有效位为0(the least significant bit )。

001100110011001100110011    # 原数值
00110011001100110011001     # 直接截断
00110011001100110011010     # 舍入,保证了最低有效位为0

IEEE-754标准给出了舍入的四种方式:

  • Round to nearest。舍入到最接近,如果真实值与舍入和不舍入的两个值距离相等(想想0.5与0和1的距离相等),那么采用偶数位优先(round to even),即让

  • Round up, or round toward plus infinity。朝正无穷大方向舍入

  • Round down, or round toward minus infinity。朝负无穷大方向舍入

  • Round toward zero, or chop, or truncate。朝0方向舍入最低有效位为0

事实上,存在更多的舍入方式,比如Python就支持多种舍入舍出方式,包括ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP,ROUND_05UP

有了以上的原理知识,就很容易理解一些奇奇怪怪的结果了,比如:

>>> 0.3 + 0.6
0.8999999999999999

2.2 双精度浮点数double

双精度浮点数的格式与单精度类似,如下:

s,符号位(1个比特)     |       e,指数位(11个比特)        |           f,有效数位(52个比特)

表示的数值为:$(-1)^s \times 1.f \times 2^{e-1023}$。

2.3 为何20141117变为20141116.了

为何20141117变为20141116.了?

>>> import torch
>>> t = torch.tensor(20141117, dtype=torch.float32)
>>> t
tensor(20141116.)

20141117的二进制表示为1001100110101010000111101(共25位),向右移动24位,1.001100110101010000111101,即$2^{24}$(由$2^{e-127}$可得e应为151,对应的二进制为10010111。小数点后有24位,但最多只能保存23位,根据舍入到最接近和偶数优先原则,有效数位为00110011010101000011110,最后一个1被截断了。

# 20141117 转换为二进制
1001100110101010000111101   # 25位

符号位:0
指数位:10010111
有效数位:00110011010101000011110

再次将数读出来,有效数位向左移动24位,得到1001100110101010000111100(最后一位为0),恰好是十进制20141116

3. 浮点数能表示最大的数

(1)单精度浮点数能表示最大的数

对于单精度浮点数来说,能表示的最大值为:$(-1)^s \times 1.f \times 2^{e-127}$

  • 符号位:0
  • 指数位:11111110,因为255用于表示无穷大或无穷小或NAN,因此$2^{e-127} = 2^{254-127} = 2^{127}$
  • 有效数位:11111111111111111111111,23个1,1.11111111111111111111111对应的十进制为1.9999998807907104

因此,单精度浮点数能表示最大的数约为$3.4028235 \times 10^{38}$。

>>> import math
>>> 1.9999998807907104 * math.pow(2, 127)
3.4028234663852886e+38

(2)什么时候会精度损失

既然单精度浮点数能表示最大的数那么大,而上述的例子,20141117远远小于这个数,为何输出却变成了20141116.。这就涉及到精度的问题。

一个数24个1,十进制为16777215,往右移动23位,恰好是1.11111111111111111111111,正好没有精度损失。我们先来验证下:

>>> torch.tensor(16777215, dtype=torch.float32)
tensor(16777215.)

>>> torch.tensor(16777216, dtype=torch.float32)
tensor(16777216.)

>>> torch.tensor(16777217, dtype=torch.float32)
tensor(16777216.)

>>> torch.tensor(16777218, dtype=torch.float32)
tensor(16777218.)

>>> torch.tensor(16777219, dtype=torch.float32)
tensor(16777220.)

可见,16777215以后,有些是准确的,有些不准确,解释如下:

# 16777215,恰好24个1,往右移动23位,有效数位全部为1
1.11111111111111111111111

# 16777216,最后一位截断
1.000000000000000000000000

# 16777217,根据偶数优先,最后一位去掉,因此输出结果变成
1.000000000000000000000001

# 16777218,最后一位截断
1.000000000000000000000010

# 16777219,根据偶数优先,舍入,因此输出的值为16777220。
1.000000000000000000000011  # 舍入前
1.00000000000000000000010   # 舍入后

4. 解决方法

了解了以上的原理,解决问题的方法就简单了,提升精度,从torch.float32torch.float64

日期格式YYYYmmdd,如果不显性指定数据类型,会被判断为整型。为了避免该错误,在处理数据时,将日期的格式设为YYYY-mm-dd,这样就不会转换为数值类型了。

参考资料:

[1] 一文读懂浮点数 – 知乎

[2] ARM Compiler ARM C and C++ Libraries and Floating-Point Support User Guide

[3] 在线进制转换

赞赏

微信赞赏支付宝赞赏

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注