作者简介
刘子瑛,毕业于清华大学软件学院。曾在高通、摩托罗拉等公司长期从事移动技术开发工作,现在阿里巴巴集团阿里云智能事业群从事智能互联网汽车等相关研发工作。
内容简介
第3章
张量与计算图
第2章我们尝试了深度学习的乐趣。“万丈高楼平地起”,我们看到了目标之后,并不等于有捷径达到目标,还是需要踏踏实实地从基本的元素开始学习。
Goo**e的深度学习框架名称为TensorFlow,可见Tensor张量对于这个框架的重要性。另外,张量虽然很重要,但是如何将张量组合起来也同样重要。目前主流的框架都是通过计算图的方式来将张量组织起来。我们通过静态计算图的典型案例TensorFlow和动态计算图的典型PyTorch,可以深刻地理解计算图的本质。
本章将介绍以下内容
0维张量:标量
计算图与流程控制
变量
3.1 0维张量:标量
所谓张量,从程序员的角度理解,就是一个多维的数组。根据维数不同,不同的张量有不同的别名。
0维的张量,称为标量。标量虽然*简单,但是也涉及数据类型、算术运算、逻辑运算、饱和运算等基础*作。
1维的张量,称为向量。从向量开始,我们就不得不引入Python 生态的重要工具库——
NumPy。作为静态计算图代表的TensorFlow,几乎是**实现了一套NumPy 的功能,所以它的跨语言能力很强。而PyTorch对于NumPy能支持的功能全部复用,只对NumPy缺少的功能进行补充。例如,NumPy计算无法使用GPU进行加速,而PyTorch就实现了NumPy功能的GPU加速。可以将PyTorch理解为可借用GPU进行加速的NumPy库。
2维的张量,称为矩阵。在机器学习中对于矩阵有**多的应用,这里涉及不少数学知识。本章我们只讲基本概念和编程,第4章会专门讲矩阵。
3维以上的张量是我们一般意义上所认为的真正的张量。在深度学习所用的数据中,我们基本上都用高维张量来处理,并根据需要不停地将其变换成各种形状的张量。
标量可以理解为一个数。虽然一个数从某方向来讲已是*简单的结构,但是对于机器学习来说,也涉及不少问题,如数据类型、计算精度。很多的优化也跟数据类型相关。
3.1.1 TensorFlow的数据类型
在学习数据类型之前,我们先看一下TensorFlow和PyTorch风格的数据类型对比图。如图3.1所示,TensorFlow比PyTorch多了复数类型。其实总体来说TensorFlow的类型丰富程度远胜于PyTorch。
TensorFlow的数据类型分为整型、浮点型和复数型3种,基本上对应NumPy的整型、浮点型和复数型,只不过每种类型的子类型比NumPy*少一些。而PyTorch比TensorFlow*加精简。原因是TensorFlow是静态计算图语言,相对要比较完备。而PyTorch是动态计算图语言,可以借助NumPy的功能。
图3.1 TensorFlow和PyTorch的数据类型对比
具体类型如图3.2所示。
图3.2 TensorFlow数据类型
其实,除了上面的基本类型以外,TensorFlow还支持量子化的整型,分别是8位的无符号quint8,8位有符号qint8,16位的无符号quint16,16位有符号qint16,还有32位有符号的qint32。
在TensorFlow中,基本上所有涉及数字类型的函数都可以**类型。
*为常用的就是常量。TensorFlow 是一门静态计算图语言,与普通计算机**语言一样,定义了常量、变量等常用编程结构。数字常数被TensorFlow使用之前,首先要赋值一个TensorFlow常量。例如:
>>> import tensorflow as tf
>>> a = tf.constant(1, dtype=tf.float**)
这样,a就是一个float**类型值为1的常量。常量也是张量的一种。
>>> a
这个常量是一个静态计算图,如果需要获取计算结果,需要建立一个会话来运行:
>>> sess = tf.Session()
>>> b = sess.run(a)
>>> print(b)
1.0
我们再来看复数的例子:
>>> c = 10 + 5.2j
>>> b = tf.constant(c)
将复数10+5.2j赋给c,再通过c创建一个常量。这个常量就是一个tf.complex128类型的常量。
>>> b
3.1.2 PyTorch的数据类型
PyTorch没有对应NumPy的复数类型,只支持整型和浮点型两种。种类也比TensorFlow要少一些,*加精练。PyTorch相当于带有GPU支持的NumPy,缺什么直接用NumPy即可,如图3.3所示。
PyTorch是动态计算图语言,不需要tf.constant之类的常量,数据计算之前赋给一个张量即可。建立Tensor是可以**类型的。例如:
>>> import torch as t
>>> a1 = t.tensor(1, dtype=t.float32)
>>> a1
tensor(1.)
图3.3 PyTorch数据类型
除了通过**dtype的方式,我们还可以通过直接创建相应的Tensor子类型的方式来创建Tensor。例如:
>>> a2 = t.FloatTensor(1)
>>> a2
tensor([ 0.])
具体类型对应关系如表3.1所示。
表3.1 PyTorch数据类型与张量子类型对应关系
数据类型
类型名称
类型别名
张量子类型
16位浮点数
half
float16
HalfTensor
32位浮点数
float
float32
FloatTensor
**位浮点数
dou**e
float**
Dou**eTensor
8位无符号整数
uint8
无
ByteTensor
8位带符号整数
int8
无
CharTensor
16位带符号整数
int16
short
ShortTensor
32位带符号整数
int32
int
IntTensor
**位带符号整数
int**
long
LongTensor
3.1.3 标量算术运算
标量虽然没有矩阵运算那么复杂,但它是承载算术和逻辑运算的载体。对于TensorFlow,标量算术运算还是需要先生成静态计算图,然后通过会话来运行。例如:
>>> a1 = tf.constant(1, dtype=tf.float**)
>>> a2 = tf.constant(2, dtype=tf.float**)
>>> a3 = a1 + a2
>>> print(a3)
Tensor(\"add:0\", shape=(), dtype=float**)
>>> sess.run(a3)
3.0
对于PyTorch来说,就是两个张量相加,结果还是一个张量,比TensorFlow要简单一些,也不需要会话。例如:
>>> b1 = t.tensor(3, dtype=t.float32)
>>> b2 = t.tensor(4, dtype=t.float32)
>>> b3 = b1 + b2
>>> b3
tensor(7.)
除了加减乘除之外,不管是TensorFlow还是PyTorch,都提供了足以满足需求的数学函数。例如三角函数,TensorFlow版计算余弦值:
>>> a20 = tf.constant(0.5)
>>> a21 = tf.cos(a20)
>>> sess.run(a21)
0.87758255
PyTorch版计算余弦值:
>>> b20 = t.tensor(0.5)
>>> b21 = t.cos(b20)
>>> b21
tensor(0.8776)
在NumPy里,对于数据类型相对是比较宽容的,如sqrt、NumPy既支持整数,也支持浮点数:
>>> **.sqrt(20)
4.47213595499958
>>> **.sqrt(20.0)
4.47213595499958
而对于TensorFlow和PyTorch来说,sqrt只支持浮点数,用整数则会报错。TensorFlow版的报错信息,根本不用在会话中执行,创建计算图时就报错:
>>> a22 = tf.constant(20, dtype=tf.int32)
>>> a23 = tf.sqrt(a22)
Traceback (most recent call last):
File \"\", line 1, in
…
TypeError: Value passed to parameter 'x' has DataType int32 not in list of allowed values: bfloat16, float16, float32, float**, complex**, complex128
PyTorch的报错相对简洁,直接声明,没有实现对于IntTensor类型的torch.sqrt函数:
>> b22 = t.tensor(10,dtype=t.int32)
>>> b23 = t.sqrt(b22)
Traceback (most recent call last):
File \"\", line 1, in
RuntimeError: sqrt not implemented for 'torch.IntTensor'
当然,不是每个函数都要求这么严格,例如,abs求**值函数,整数和浮点数都支持:
>>> a10 = tf.constant(10, dtype=tf.float32)
>>> a11 = tf.abs(a10)
>>> sess.run(a11)
10.0
>>> a12 = tf.constant(20, dtype=tf.int32)
>>> a13 = tf.abs(a12)
>>> sess.run(a13)
20
3.1.4 Tensor与NumPy类型的转换
不管TensorFlow还是PyTorch,都跟NumPy有很深的渊源,它们之间的转换也**重要。
1.PyTorch与NumPy之间交换数据
PyTorch有统一的接口用于与NumPy交换数据。PyTorch的张量(Tensor)可以通过NumPy 函数来转换成NumPy的数组(ndarray)。例如:
>>> b10 = t.tensor(0.2, dtype=t.float**)
>>> c10 = b10.NumPy()
>>> c10
array(0.2)
>>> b10
tensor(0.2000, dtype=torch.float**)
作为逆运算,对于一个NumPy 的数组对象,可以通过torch.from_numpy 函数转换成PyTorch的张量。例如:
>>> c11 = **.array([[1,0],[0,1]])
>>> c11
array([[1, 0],
[0, 1]])
>>> b11 = t.from_NumPy(c11)
>>> b11
tensor([[ 1, 0],
[ 0, 1]])
2.TensorFlow与NumPy之间的数据交换
TensorFlow与NumPy就是构造计算图与执行计算图的过程。将NumPy的数组转成TensorFlow 的Tensor,可以通过tf.constant 来构造一个计算图。而作为逆运算的从TensorFlow的张量转换成NumPy的数组,可以通过创建一个新会话运行计算图。
示例1,从ndarray到Tensor:
>>> a11
array([[1, 0],
[0, 1]])
>>> c11 = tf.constant(a11)
>>> c11
示例2,从Tensor到ndarray:
>>> a11 = sess.run(c11)
>>> a11
array([[1, 0],
[0, 1]])
另外,TensorFlow还提供了一系列的转换函数。
例如,to_int32函数可以将一个Tensor的值转换为32位整数:
>>> a01 = tf.consant(0, tf.int32)
>>> a02 = tf.to_int32(a01)
>>> sess.run(a02)
0
类似的函数还有tf.to_int**、tf.to_float、tf.to_dou**e等,基本上每个主要类型都有一个。定义这么多函数太麻烦,但还是有通用的转换函数tf.cast。格式为:tf.cast(Tensor, 类型名)。例如:
>>> b05 = tf.cast(b02, tf.complex128)
>>> sess.run(b05)
(1+0j)
3.TensorFlow的饱和数据转换
TensorFlow定义了这么多转换函数,有什么好处?答案是功能多。以TensorFlow还支持饱和转换为例,我们将大类型如int**转换成小类型int16,tf.cast转换过程中可能产生溢出,这在机器学习的计算中是件可怕的事情,而使用饱和转换,*多是变成小类型的*大值,而不会变成负值。
例如,把65536转换成tf.int8类型。我们知道,int8只能表示-128到127之间的数。使用饱和转换tf.saturate_cast,只要是大于127的数值,转换出来就是127,不会*大。
例如:
>>> b06 = tf.constant(65536,dtype=tf.int**)
>>> sess.run(b06)
65536
>>> b07 = tf.saturate_cast(b06,tf.int8)
>>> sess.run(b07)
127
3.2 计算图与流程控制
计算图是一种特殊的有向无环图DAG,用来表示变量和*作之间的关系。它有点类似于流程图,但是比流程图多了对变量的描述。
既然与流程图类似,那么计算图其实相当于是一种计算机语言,需要有完备的计算机语言的功能。我们知道,一种结构化的计算机语言,除了顺序结构之外,还需要有分支结构和循环结构。下面,我们分别看下静态计算图及其代表TensorFlow与动态计算图及PyTorch如何实现计算图。
3.2.1 静态计算图与TensorFlow
静态计算图有点像编译型的语言,首先要写好完整的代码,然后再编译成机器指令并执行。会话中运行,相当于在TensorFlow机器上运行。
所以TensorFlow的静态计算图语言中,需要包括完整的指令集。因为不能借助宿主机,需要有条件分支指令、循环指令,甚至为了辅助开发,还需要一些调试指令。具体如图3.4所示。
图3.4 TensorFlow主要流程控制功能
1.比较运算
分支的**步是要有比较指令。*简单的指令当然是判断是否相等,TensorFlow提供了equal函数来实现此功能,例如:
>>> c1 = tf.constant(1)
>>> c2 = tf.constant(2)
>>> c3 = tf.equal(c1,c2)
>>> sess.run(c3)
False
结果返回False,说明c1和c2这两个张量不相等。
我们用判断不相等的not_equal函数来比较,结果就会为True,如下例:
>>> c4 = tf.not_equal(c1,c2)
>>> sess.run(c4)
True
如果比较大小,可以用小于less和大于greater两个函数,如下例:
>>> c5 = tf.less(c1,c2)
>>> sess.run(c5)
True
>>> c6 = tf.greater(c1,c2)
>>> sess.run(c6)
False
除了相等、不等、大于、小于之外,还有大于或等于*作greater_equal和小于或等于*作less_equal,我们看两个例子:
>>> c7 = tf.less_equal(c1,c2)
>>> sess.run(c7)
True
>>> c8 = tf.greater_equal(c1,c2)
>>> sess.run(c8)
False
另外,比较大小还可以批量进行,判断条件是一个布尔型的数组,后面是根据不同情况赋的值,这是where*作的功能。我们在后面学习向量之时再详细介绍。
2.逻辑运算
通过上面的6种比较运算,我们可以在TensorFlow的计算流程图中对比较运算的结果进行逻辑运算。
逻辑运算一共有以下4种:
? 与:logical_and;
? 或:logical_or;
? 非:logical_not;
? 异或:logical_xor。
我们举几个例子来介绍,首先是取非的例子:
>>> c9 = tf.logical_not(tf.greater_equal(c1,c2))
>>> sess.run(c9)
True
再用greater、equal和logical_or来实现greater_equal功能,代码如下:
>>> c11 = tf.constant(1)
>>> c12 = tf.constant(2)
>>> c13 = tf.logical_or(tf.greater(c11,c12), tf.equal(c11,c12))
>>> sess.run(c13)
False
例如,我们用数字来调用logical_xor,代码如下:
c10 = tf.logical_xor(1,2)
将报错如下:
TypeError: Expected bool, got 1 of type 'int' instead.
如果用两个整数类型的张量去进行逻辑运算,将报错如下:
ValueError: Tensor conversion requested dtype bool for Tensor with dtype int32: 'Tensor(\"Const:0\", shape=(), dtype=int32)'
习惯了C语言的弱类型的读者请特别注意以上情况。
3.流程控制——分支结构
有很多人在学到TensorFlow中还有流程控制*作时觉得很奇怪,这是对于静态计算图的理解还不够清楚的体现。再次强调一下,静态计算图就是一门程序设计语言,所以流程控制是**重要的基本功能。
流程控制的基础是分支功能,就像Python中的if判断一样。在TensorFlow中,可以用case*作来实现。
我们一步步来展示case的功能。*简单的case语句只有一个判断条件和一个对应和函数。我们来看例子:
>>> d1 = tf.constant(1)
>>> d2 = tf.case([(tf.greater(d1,0), lambda : tf.constant(0))])
>>> sess.run(d2)
0
解释一下上述语句,如果d1大于0,执行后面的lambda函数,返回一个值为0的常量。
下面可以给case语句增加一个default分支:
>>> d3 = tf.case([(tf.greater(d1,0), lambda : tf.constant(0))], default=lambda : tf.constant(-1))
>>> sess.run(d3)
0
然后尝试加多个分支:
>>> d4 = tf.case([(tf.greater(d1,1), lambda : tf.constant(2)),(tf.equal(d1,1),lambda : tf.constant(1))], default=lambda : tf.constant(-1))
>>> sess.run(d4)
1
如果d1大于1,返回值为2的常量。如果d1等于1,则返回值为1的常量。
4.流程控制——循环结构
我们来看看循环结构,*常用的循环是for循环,对应TensorFlow的*作是while_loop*作。我们看个例子:
>>> e0 = tf.constant(1)
>>> e1 = tf.while_loop(lambda i : tf.less(i,10), lambda j : tf.add(j,1), [e0])
>>> sess.run(e1)
我们定义e0用作循环控制变量,while_loop*作*少需要3个参数,**个参数是结束循环的判断条件,第二个参数是循环体,第三个参数是循环控制变量。
所谓循环体,是需要循环多次*作的具体功能,在我们这个例子中,是一个给循环控制变量加1 的*作。
循环控制变量可以设计得很复杂,它会作为参数传给前面的**个和第二个calla**e参数。例如,**个参数,我们给的是一个lambda表达式,并没有**要传的参数,形参的名称与实际传入的无关。循环控制变量参数获取之后,才会把这个参数传给lambda表达式去执行。
5.程序调试
当我们写了比较复杂的流程控制到静态计算图中,需要一些调试手段来测试逻辑是否正确。
*基础的功能是可以打印输出debug信息,这可以通过Print函数实现。例如:
>>> e2 = tf.Print(tf.constant(0),['Debug info'])
(1)一切以代码说话:本书的一切原理都有相应的代码实现。一时不能理解的原理,可以通过实践慢慢体会,能够让程序员以较低的成本迅速入门。
(2)从现象到本质:世界上除了这两种主流框架之外,还有微软的cntk、***的mxnet、百度的paddlepaddle等。其实本质上它们都趋同的,有了本书学习的基础,寄希读者可以*高维度地思考框架背后的设计理念,有所取舍,而不沦为其奴仆。
(3)5-4-6 速成法:深度学习用到的数学知识很多,概念也很多,学习曲线很陡。但是,笔者还是从中抓住了一条主线——计算图模型。基于计算图模型,总结出了5-4-6 速成法,通过5 步,使用4 种基本元素,组合6 种基本网络结构,就能够写出功能**强大的深度学习程序。
(4)**深度学习:本书还讲解了深度学习的两个重要应用:如何自动调参和深度学习引发的强化学习。可以看到,编程变得越来越简单,但是系统变得越来越复杂。我们一方面要时刻关注它们的进展,另一方面手工写神经网络的基本功还不能丢。
(5)赠送资源丰富:本书配套的源代码+100分钟配套学习视频+相关技术延伸阅读。