关于半精度FP16的表示范围和精度、混合精度训练--彻底弄懂

摘要:之前想看一下浮点数和整型数的表示到底有什么区别,零零散散看了一些文章,感觉写得都不得要领,今天就系统把FP16的表示原理,以及非规格化数完全讲明白。

1、首先说一下二进制小数和十进制小数之间的转化
(1)先说二进制转十进制

小数部分从小数点位置开始往后依次表示为:1/2,1/4,1/8,1/16 ...

那么:1010.1011表示为十进制就是8+2+1/2+1/8+1/16 
    即:1101.0111 = 1*2^3 + 1*2^2 + 0*2^1 + 1*2^0 + 0*2^(-1) + 1*2^(-2) + 1*2^(-3) + 1*2^(-4) = 10.6875

2)十进制转二进制:

    比如要把0.4375转为二进制

    0.4375*2=0.875 整数部分为0,即二进制小数第一位为0,当前二进制数值为:0.0 

    0.875*2=1.75 整数部分为1 即当前二进制数值为:0.01 去掉1后继续运算。 

    0.75*2=1.5 整数部分为1 即当前二进制数值为:0.011 去掉1后继续运算。 

    0.5*2=1.0 整数部分为1 即当前二进制数值为:0.0111 去掉1后为0,运算结束。

    则0.4375表示为二进制数就是0.0111

2、再来看看float16的表示方法

fp16顾名思义有16位二进制数,其中有一位固定的符号位,记为s。剩下15位中5位作为指数,记为e。剩下的10位用于表示小数,记为m。首先看一下fp16的表示公式:

(-1)^s * 2^(e-b) * (1+m/2^10)

其中b是指数偏置,根据IEEE754标准一般把b定为2^(e-1)-1,那么这里e=5带入就得到b=15。公式后面的2^10中10就是小数的位数,10位。那么上面公式可以化简为:

(-1)^s * 2^(e-15) * (1+m/1024) # 公式中的s,e,m都是十进制数

s是符号位,那不是0就是1,

e是指数位范围就在0~2^5之间,即0~32 (其实这样说不准确,后面会说明)

m是小数的范围,在0~1024之间,m虽然表示小数位,但是在这个公式里面还是整数。

注意,一个正常的数据表示方法,不仅要能表示正常的整数小数,还要能表示NaN,无穷,那么上面的公式就需要份情况讨论:

(1)e全为0,m全为0,表示数字0

(2)e全为0,m不全为0,表示一个很小的数,计算方式为: (-1)^s * 2^(-14) * (0+m/2^10)  # 注意加号前为0,这里为什么是-14而非-15其实跟上面的(0+m/2^10)中的0有关,可以看下面第7小节

(3)e全为1,m全为0,表示正负无穷

(4)e全为1,m不全为0,表示NAN

也就是说表示正常数字的话,指数位最高只能到达30,即11110,到不了31,因为指数全1就表示特殊数字了。

3、float16的表示范围

根据上面的公式,fp16表示的最大数为:指数位是11110即30,小数位为全1,即1023/1024

那么最大数maxnum = 2^(30-15) * (1+1023/1024)=65504

那么最小数minnum = 2^(0-14) * (0+1/1024)=2^(-24) ,至于为啥不是2^(-25),可以参考下面的第7小节。注意这里说的最大最小都是指绝对值。

4、关键问题--fp16的表示精度

这个问题也是之前困扰我时间最长的问题,我之所以看了很多帖子没看明白,是因为我一直以整体数的思维方式来考虑fp16了。我之前就以为fp16就是从0~65504之间都能用同样的精度表示,但其实不是的,简单概括,浮点数表示的数字越大,精度越低(其实这里的精度应该交保真度)

首先举个例子,你在python代码里面写a=1.24然后把a转成半精度float16的格式再输出,a = np.float16(a)会发现还是1.24,嗯,,这没得问题。然后你把b=2049也转成float16格式输出,你会发现输出是2048.0,问题来了,这float16这么智障吗,几千的数字都表示不了??那有什么用,是不是哪里搞错了??其实没有搞错,float16就是这样的,就是表示不了2049,所以我们正常用的都是float32,float16一般我们平常用会很少接触到,混合精度训练中会用到(float16和float32混合)

然后来说一下为什么float16不能表示2049呢。Float16确实可以表示2^(-14)~65504之间的数(指数位决定表示范围),大概就是2^(-14)~2^16之间,float16在表示数字的保真度是这样的:

在2^(-14)~2^16之间有30个区间:2^(-14)~2^(-13), 2^(-13)~2^(-12), ......, 2^(-1)~1, 1~2, 2~4, ......, 2^15~2^16。其实不管区间的大小,float都会把这个区间分为2^m即1024份,在2^(-14)~2^(-13)之间分成1024份,在2^15~2^16之间也分成1024份。那么上面的问题为什么2049会被表示乘2048就很清楚了,因为2049在2^11~2^12之间,即在2048~4096之间,这之间有2048个整数,但是这2048个数只能被表示1024次,所以只能隔2个表示一个,同理在2^15~2^16之间有2^15个数,那就只能每隔32个数表示一次了。

5、截断误差和舍入误差

 为什么两数相加容易产生舍入误差,比如两个fp16的数相加: 2049+0.01, 那么结果就是2048了,即两数当中较小的数倍舍弃了。怪不得在混合精度训练时乘法可以用低精度,而加法必须要用高精度呢。截断误差就是表示的数超范围了无法表示。

    

6、关于浮点数的上溢和下溢:  2022-9-21 10:49:28

上溢:是指由于数字过大,超过当前类型所能表示的范围

下溢:是指由于数值太小,低于当前类型所能表示的最小的值,计算机就只好把尾数位向右移,空出第一个二进制位,但是与此同时,却损失了原来末尾有效位上面的数字,这种情况就叫做下溢。

7、关于FP16的非规格化数表示

首先,根据FP16的公式:

(-1)^s * 2^(e-15) * (1+m/2^10)

要使FP16表示最小的正数,那么5位指数都要是0,即e=0,那么指数部分是2^(-15) ;小数部分应该是1,即m=1,符号位+指数位+小数位完整写出来就是:

0 00000 0000000001

注意!!!小数位虽然写的是0000000001,但是因为小数的首位隐藏位都是1,实际上这里的小数是 1.0000000001,即1+1/1024,如果不做任何修改,那么FP16能 表示的最小正数就是: 2^(-15)*(1+1/1024)

这显然是不够小的,所以要启动非规格化数表示法,就是把1.0000000001前面的1进位给指数,让指数从2^(-15)变为2^(-14),而小数部分就相当于变成0.0000000001,即1/1024,所以能表示的最小正数就变为2^(-14)*(1/1024)=2^(-24)

关于借位,应该是参考的十进制的科学计数法,比如123可以表示为1.23*10^2,正数增加一位就变成了0.123*10^3

  • 28
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值