IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    计算机如何存储浮点数

    SparkAndShine发表于 2021-09-09 22:12:20
    love 0

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

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

    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.float32到torch.float64。

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

    参考资料:

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

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

    [3] 在线进制转换



沪ICP备19023445号-2号
友情链接