本文讲解符号数存储格式,能表示最大的值是多少,在什么情况下会出现精度损失。
遇到一个问题。我使用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
,将日期存储为单精度浮点数,精度损失导致。
计算机存储浮点数采用IEEE-754 标准,分为单精度浮点数float(32位)和双精度浮点数double(64位)。
单精度存储格式如下:
s,符号位(1个比特) | e,指数位(8个比特) | f,有效数位(23个比特)
表示的数值为:$(-1)^s \times 1.f \times 2^{e-127}$,其中,
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位,有两种取法:
00110011001100110011001
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
双精度浮点数的格式与单精度类似,如下:
s,符号位(1个比特) | e,指数位(11个比特) | f,有效数位(52个比特)
表示的数值为:$(-1)^s \times 1.f \times 2^{e-1023}$。
为何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
。
(1)单精度浮点数能表示最大的数
对于单精度浮点数来说,能表示的最大值为:$(-1)^s \times 1.f \times 2^{e-127}$
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 # 舍入后
了解了以上的原理,解决问题的方法就简单了,提升精度,从torch.float32
到torch.float64
。
日期格式YYYYmmdd
,如果不显性指定数据类型,会被判断为整型。为了避免该错误,在处理数据时,将日期的格式设为YYYY-mm-dd
,这样就不会转换为数值类型了。
参考资料:
[1] 一文读懂浮点数 – 知乎
[2] ARM Compiler ARM C and C++ Libraries and Floating-Point Support User Guide
[3] 在线进制转换