第2章【预备知识】--动手学深度学习【Tensorflow2.0版本】

项目地址:https://github.com/TrickyGo/Dive-into-DL-TensorFlow2.0

UC 伯克利李沐的《动手学深度学习》开源书一经推出便广受好评。很多开发者使用了书的内容,并采用各种各样的深度学习框架将其复现。
现在,《动手学深度学习》书又有了一个新的复现代码版本——TensorFlow2.0 版,短时间内成为了github上千star项目,欢迎关注。

2.1 环境配置

本节简单介绍一些必要的软件的安装与配置,由于不同机器软硬件配置不同,所以不详述,遇到问题请善用Google。

2.1.1 Anaconda

Anaconda是Python的一个开源发行版本,主要面向科学计算。我们可以简单理解为,Anaconda是一个预装了很多我们用的到或用不到的第三方库的Python。而且相比于大家熟悉的pip install命令,Anaconda中增加了conda install命令。当你熟悉了Anaconda以后会发现,conda install会比pip install更方便一些。
强烈建议先去看看最省心的Python版本和第三方库管理——初探Anaconda初学 Python 者自学 Anaconda 的正确姿势-猴子的回答

总的来说,我们应该完成以下几步:

  • 根据操作系统下载并安装Anaconda(或者mini版本Miniconda)并学会常用的几个conda命令,例如如何管理python环境、如何安装卸载包等;
  • Anaconda安装成功之后,我们需要修改其包管理镜像为国内源,这样以后安装包时就会快一些。

2.1.2 Jupyter

在没有notebook之前,在IT领域是这样工作的:在普通的 Python shell 或者在IDE(集成开发环境)如Pycharm中写代码,然后在word中写文档来说明你的项目。这个过程很繁琐,通常是写完代码,再写文档的时候我还的重头回顾一遍代码。最蛋疼的地方在于,有些数据分析的中间结果,还得重新跑代码,然后把结果弄到文档里给客户看。有了notebook之后,世界突然美好了许多,因为notebook可以直接在代码旁写出叙述性文档,而不是另外编写单独的文档。也就是它可以能将代码、文档等这一切集中到一处,让用户一目了然。如下图所示。
在这里插入图片描述
Jupyter Notebook 已迅速成为数据分析,机器学习的必备工具。因为它可以让数据分析师集中精力向用户解释整个分析过程。

我们参考jupyter notebook-猴子的回答进行jupyter notebook及常用包(例如环境自动关联包nb_conda)的安装。

安装好后,我们使用以下命令打开一个jupyter notebook:

jupyter notebook 

这时在浏览器打开 http://localhost:8888 (通常会自动打开)位于当前目录的jupyter服务。

2.1.3 Tensorflow

由于本文需要用到Tensorflow框架,所以还需要安装Tensorflow(后期必不可少地会使用GPU,所以安装GPU版本的)。直接去Tensorflow安装页找到自己的软硬件对应的安装命令即可。安装好后使用以下命令可查看安装的Tensorflow及版本号。

conda list tensorflow

2.1.4 其他

此外还可以安装python最好用的IDE PyCharm,专业版的应该是需要收费的,但学生用户可以申请免费使用(传送门),或者直接用免费的社区版。

如果不喜欢用IDE也可以选择编辑器,例如VSCode等。

本节与原文有很大不同,原文传送门

import tensorflow as tf
print(tf.__version__)
2.0.0

2.2 数据操作

在深度学习中,我们通常会频繁地对数据进行操作。作为动手学深度学习的基础,本节将介绍如何对内存中的数据进行操作。

在TensorFlow中,tensor是一个类,也是存储和变换数据的主要工具。如果你之前用过NumPy,你会发现tensor和NumPy的多维数组非常类似。然而,tensor提供GPU计算和自动求梯度等更多功能,这些使tensor更加适合深度学习。

2.2.1 创建 tensor

我们先介绍tensor的最基本功能,我们用arange函数创建一个行向量。

x = tf.constant(range(12))

print(x.shape)
(12,)
x
<tf.Tensor: id=0, shape=(12,), dtype=int32, numpy=array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])>

这时返回了一个tensor实例,其中包含了从0开始的12个连续整数。

我们可以通过shape属性来获取tensor实例的形状。

x.shape
TensorShape([12])

我们也能够通过len得到tensor实例中元素(element)的总数。

len(x)
12

下面使用reshape函数把行向量x的形状改为(3, 4),也就是一个3行4列的矩阵,并记作X。除了形状改变之外,X中的元素保持不变。

X = tf.reshape(x,(3,4))
X
<tf.Tensor: id=2, shape=(3, 4), dtype=int32, numpy=
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])>

注意X属性中的形状发生了变化。上面x.reshape((3, 4))也可写成x.reshape((-1, 4))x.reshape((3, -1))。由于x的元素个数是已知的,这里的-1是能够通过元素个数和其他维度的大小推断出来的。

接下来,我们创建一个各元素为0,形状为(2, 3, 4)的张量。实际上,之前创建的向量和矩阵都是特殊的张量。

tf.zeros((2,3,4))
<tf.Tensor: id=5, shape=(2, 3, 4), dtype=float32, numpy=
array([[[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]],

       [[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]]], dtype=float32)>

类似地,我们可以创建各元素为1的张量。

tf.ones((3,4))
<tf.Tensor: id=8, shape=(3, 4), dtype=float32, numpy=
array([[1., 1., 1., 1.],
       [1., 1., 1., 1.],
       [1., 1., 1., 1.]], dtype=float32)>

我们也可以通过Python的列表(list)指定需要创建的tensor中每个元素的值。

Y = tf.constant([[2,1,4,3],[1,2,3,4],[4,3,2,1]])
Y
<tf.Tensor: id=9, shape=(3, 4), dtype=int32, numpy=
array([[2, 1, 4, 3],
       [1, 2, 3, 4],
       [4, 3, 2, 1]])>

有些情况下,我们需要随机生成tensor中每个元素的值。下面我们创建一个形状为(3, 4)的tensor。它的每个元素都随机采样于均值为0、标准差为1的正态分布。

tf.random.normal(shape=[3,4], mean=0, stddev=1)
<tf.Tensor: id=15, shape=(3, 4), dtype=float32, numpy=
array([[-0.06770465, -2.518872  ,  0.5077952 ,  0.6465717 ],
       [ 0.2775639 ,  0.15904428, -0.38203633,  0.30524674],
       [-0.16228472, -0.7232593 ,  0.11517206, -0.49598092]],
      dtype=float32)>

2.2.2 运算

tensor支持大量的运算符(operator)。例如,我们可以对之前创建的两个形状为(3, 4)的tensor做按元素加法。所得结果形状不变。

X + Y
<tf.Tensor: id=16, shape=(3, 4), dtype=int32, numpy=
array([[ 2,  2,  6,  6],
       [ 5,  7,  9, 11],
       [12, 12, 12, 12]])>

按元素乘法:

X * Y
<tf.Tensor: id=17, shape=(3, 4), dtype=int32, numpy=
array([[ 0,  1,  8,  9],
       [ 4, 10, 18, 28],
       [32, 27, 20, 11]])>

按元素除法:

X / Y
<tf.Tensor: id=20, shape=(3, 4), dtype=float64, numpy=
array([[ 0.  ,  1.  ,  0.5 ,  1.  ],
       [ 4.  ,  2.5 ,  2.  ,  1.75],
       [ 2.  ,  3.  ,  5.  , 11.  ]])>

按元素做指数运算:

Y = tf.cast(Y, tf.float32)
tf.exp(Y)
<tf.Tensor: id=22, shape=(3, 4), dtype=float32, numpy=
array([[ 7.389056 ,  2.7182817, 54.598152 , 20.085537 ],
       [ 2.7182817,  7.389056 , 20.085537 , 54.598152 ],
       [54.598152 , 20.085537 ,  7.389056 ,  2.7182817]], dtype=float32)>

除了按元素计算外,我们还可以使用matmul函数做矩阵乘法。下面将XY的转置做矩阵乘法。由于X是3行4列的矩阵,Y转置为4行3列的矩阵,因此两个矩阵相乘得到3行3列的矩阵。

Y = tf.cast(Y, tf.int32)
tf.matmul(X, tf.transpose(Y))
<tf.Tensor: id=26, shape=(3, 3), dtype=int32, numpy=
array([[ 18,  20,  10],
       [ 58,  60,  50],
       [ 98, 100,  90]])>

我们也可以将多个tensor连结(concatenate)。下面分别在行上(维度0,即形状中的最左边元素)和列上(维度1,即形状中左起第二个元素)连结两个矩阵。可以看到,输出的第一个tensor在维度0的长度( 6 )为两个输入矩阵在维度0的长度之和( 3+3 ),而输出的第二个tensor在维度1的长度( 8 )为两个输入矩阵在维度1的长度之和( 4+4 )。

tf.concat([X,Y],axis = 0), tf.concat([X,Y],axis = 1)
(<tf.Tensor: id=28, shape=(6, 4), dtype=int32, numpy=
 array([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [ 2,  1,  4,  3],
        [ 1,  2,  3,  4],
        [ 4,  3,  2,  1]])>,
 <tf.Tensor: id=30, shape=(3, 8), dtype=int32, numpy=
 array([[ 0,  1,  2,  3,  2,  1,  4,  3],
        [ 4,  5,  6,  7,  1,  2,  3,  4],
        [ 8,  9, 10, 11,  4,  3,  2,  1]])>)

使用条件判断式可以得到元素为0或1的新的tensor。以X == Y为例,如果X和Y在相同位置的条件判断为真(值相等),那么新的tensor在相同位置的值为1;反之为0。

tf.equal(X,Y)
<tf.Tensor: id=31, shape=(3, 4), dtype=bool, numpy=
array([[False,  True, False,  True],
       [False, False, False, False],
       [False, False, False, False]])>

tensor中的所有元素求和得到只有一个元素的tensor

tf.reduce_sum(X)
<tf.Tensor: id=33, shape=(), dtype=int32, numpy=66>
X = tf.cast(X, tf.float32)
tf.norm(X)
<tf.Tensor: id=39, shape=(), dtype=float32, numpy=22.494444>

2.2.3 广播机制

前面我们看到如何对两个形状相同的tensor做按元素运算。当对两个形状不同的tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个tensor形状相同后再按元素运算。

定义两个tensor

A = tf.reshape(tf.constant(range(3)), (3,1))
B = tf.reshape(tf.constant(range(2)), (1,2))
A, B
(<tf.Tensor: id=42, shape=(3, 1), dtype=int32, numpy=
 array([[0],
        [1],
        [2]])>,
 <tf.Tensor: id=45, shape=(1, 2), dtype=int32, numpy=array([[0, 1]])>)

由于AB分别是3行1列和1行2列的矩阵,如果要计算A + B,那么A中第一列的3个元素被广播(复制)到了第二列,而B中第一行的2个元素被广播(复制)到了第二行和第三行。如此,就可以对2个3行2列的矩阵按元素相加。

A + B
<tf.Tensor: id=46, shape=(3, 2), dtype=int32, numpy=
array([[0, 1],
       [1, 2],
       [2, 3]])>

2.2.4 索引

tensor中,索引(index)代表了元素的位置。tensor的索引从0开始逐一递增。例如,一个3行2列的矩阵的行索引分别为0、1和2,列索引分别为0和1。

在下面的例子中,我们指定了tensor的行索引截取范围[1:3]。依据左闭右开指定范围的惯例,它截取了矩阵X中行索引为1和2的两行。

X[1:3]
<tf.Tensor: id=50, shape=(2, 4), dtype=float32, numpy=
array([[ 4.,  5.,  6.,  7.],
       [ 8.,  9., 10., 11.]], dtype=float32)>

我们可以指定tensor中需要访问的单个元素的位置,如矩阵中行和列的索引,并为该元素重新赋值。

X = tf.Variable(X)
X[1,2].assign(9)
<tf.Variable 'UnreadVariable' shape=(3, 4) dtype=float32, numpy=
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  9.,  7.],
       [ 8.,  9., 10., 11.]], dtype=float32)>

当然,我们也可以截取一部分元素,并为它们重新赋值。在下面的例子中,我们为行索引为1的每一列元素重新赋值。

X = tf.Variable(X)
X
<tf.Variable 'Variable:0' shape=(3, 4) dtype=float32, numpy=
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  9.,  7.],
       [ 8.,  9., 10., 11.]], dtype=float32)>
X[1:2,:].assign(tf.ones(X[1:2,:].shape, dtype = tf.float32)*12)
<tf.Variable 'UnreadVariable' shape=(3, 4) dtype=float32, numpy=
array([[ 0.,  1.,  2.,  3.],
       [12., 12., 12., 12.],
       [ 8.,  9., 10., 11.]], dtype=float32)>

2.2.5 运算的内存开销

在前面的例子里我们对每个操作新开内存来存储运算结果。举个例子,即使像Y = X + Y这样的运算,我们也会新开内存,然后将Y指向新内存。为了演示这一点,我们可以使用Python自带的id函数:如果两个实例的ID一致,那么它们所对应的内存地址相同;反之则不同。

X = tf.Variable(X)
Y = tf.cast(Y, dtype=tf.float32)

before = id(Y)
Y = Y + X
id(Y) == before
False

如果想指定结果到特定内存,我们可以使用前面介绍的索引来进行替换操作。在下面的例子中,我们先通过zeros_like创建和Y形状相同且元素为0的tensor,记为Z。接下来,我们把X + Y的结果通过[:]写进Z对应的内存中。

Z = tf.Variable(tf.zeros_like(Y))
before = id(Z)
Z[:].assign(X + Y)
id(Z) == before
True

实际上,上例中我们还是为X + Y开了临时内存来存储计算结果,再复制到Z对应的内存。如果想避免这个临时内存开销,我们可以使用运算符全名函数中的out参数。

Z = tf.add(X, Y)
id(Z) == before
False

如果X的值在之后的程序中不会复用,我们也可以用 X[:] = X + Y 或者 X += Y 来减少运算的内存开销。

before = id(X)
X.assign_add(Y)
id(X) == before
True

2.2.6 tensor 和 NumPy 相互变换

我们可以通过array函数和asnumpy函数令数据在NDArray和NumPy格式之间相互变换。下面将NumPy实例变换成tensor实例。

import numpy as np

P = np.ones((2,3))
D = tf.constant(P)
D
<tf.Tensor: id=115, shape=(2, 3), dtype=float64, numpy=
array([[1., 1., 1.],
       [1., 1., 1.]])>

再将NDArray实例变换成NumPy实例。

np.array(D)
array([[1., 1., 1.],
       [1., 1., 1.]])

2.3 自动求梯度

import tensorflow as tf
print(tf.__version__)

# import os
# os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# tf.test.is_gpu_available( cuda_only=False, min_cuda_compute_capability=None )
2.0.0

在深度学习中,我们经常需要对函数求梯度(gradient)。本节将介绍如何使用tensorflow2.0提供的GradientTape来自动求梯度。

2.3.1 简单示例

我们先看一个简单例子:对函数 y = 2 x x y = 2\boldsymbol{x}^{\top}\boldsymbol{x} 求关于列向量 x \boldsymbol{x} 的梯度。我们先创建变量x,并赋初值。

x = tf.reshape(tf.Variable(range(4), dtype=tf.float32),(4,1))
x
<tf.Tensor: id=10, shape=(4, 1), dtype=float32, numpy=
array([[0.],
       [1.],
       [2.],
       [3.]], dtype=float32)>

函数 y = 2 x x y = 2\boldsymbol{x}^{\top}\boldsymbol{x} 关于 x \boldsymbol{x} 的梯度应为 4 x 4\boldsymbol{x} 。现在我们来验证一下求出来的梯度是正确的。

with tf.GradientTape() as t:
    t.watch(x)
    y = 2 * tf.matmul(tf.transpose(x), x)
    
dy_dx = t.gradient(y, x)
dy_dx
<tf.Tensor: id=30, shape=(4, 1), dtype=float32, numpy=
array([[ 0.],
       [ 4.],
       [ 8.],
       [12.]], dtype=float32)>

2.3.2 训练模式和预测模式

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    y = x * x
    z = y * y
    dz_dx = g.gradient(z, x)  # 108.0 (4*x^3 at x = 3)
    dy_dx = g.gradient(y, x)  # 6.0
dz_dx,dy_dx
WARNING:tensorflow:Calling GradientTape.gradient on a persistent tape inside its context is significantly less efficient than calling it outside the context (it causes the gradient ops to be recorded on the tape, leading to increased CPU and memory usage). Only call GradientTape.gradient inside the context if you actually want to trace the gradient in order to compute higher order derivatives.
WARNING:tensorflow:Calling GradientTape.gradient on a persistent tape inside its context is significantly less efficient than calling it outside the context (it causes the gradient ops to be recorded on the tape, leading to increased CPU and memory usage). Only call GradientTape.gradient inside the context if you actually want to trace the gradient in order to compute higher order derivatives.

(<tf.Tensor: id=41, shape=(4, 1), dtype=float32, numpy=
 array([[  0.],
        [  4.],
        [ 32.],
        [108.]], dtype=float32)>,
 <tf.Tensor: id=47, shape=(4, 1), dtype=float32, numpy=
 array([[0.],
        [2.],
        [4.],
        [6.]], dtype=float32)>)

2.3.3 对Python控制流求梯度

即使函数的计算图包含了Python的控制流(如条件和循环控制),我们也有可能对变量求梯度。

考虑下面程序,其中包含Python的条件和循环控制。需要强调的是,这里循环(while循环)迭代的次数和条件判断(if语句)的执行都取决于输入a的值。

def f(a):
    b = a * 2
    while tf.norm(b) < 1000:
        b = b * 2
    if tf.reduce_sum(b) > 0:
        c = b
    else:
        c = 100 * b
    return c

我们来分析一下上面定义的f函数。事实上,给定任意输入a,其输出必然是 f(a) = x * a的形式,其中标量系数x的值取决于输入a。由于c = f(a)有关a的梯度为x,且值为c / a,我们可以像下面这样验证对本例中控制流求梯度的结果的正确性。

a = tf.random.normal((1,1),dtype=tf.float32)
with tf.GradientTape() as t:
    t.watch(a)
    c = f(a)
t.gradient(c,a) == c/a
<tf.Tensor: id=201, shape=(1, 1), dtype=bool, numpy=array([[ True]])>

查阅文档

受篇幅所限,本书无法对所有用到的tensorflow2.0函数和类一一详细介绍。读者可以查阅相关文档来做更深入的了解。

import tensorflow as tf
print(tf.__version__)
2.0.0

2.4.1 search for functions and classes

当我们想知道一个模块里面提供了哪些可以调用的函数和类的时候,可以使用dir函数。下面我们打印dtypesrandom模块中所有的成员或属性。

dir(tf.dtypes)
['DType',
 'QUANTIZED_DTYPES',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '_sys',
 'as_dtype',
 'bfloat16',
 'bool',
 'cast',
 'complex',
 'complex128',
 'complex64',
 'double',
 'float16',
 'float32',
 'float64',
 'half',
 'int16',
 'int32',
 'int64',
 'int8',
 'qint16',
 'qint32',
 'qint8',
 'quint16',
 'quint8',
 'resource',
 'saturate_cast',
 'string',
 'uint16',
 'uint32',
 'uint64',
 'uint8',
 'variant']
dir(tf.random)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '_sys',
 'all_candidate_sampler',
 'categorical',
 'experimental',
 'fixed_unigram_candidate_sampler',
 'gamma',
 'learned_unigram_candidate_sampler',
 'log_uniform_candidate_sampler',
 'normal',
 'poisson',
 'set_seed',
 'shuffle',
 'stateless_categorical',
 'stateless_normal',
 'stateless_truncated_normal',
 'stateless_uniform',
 'truncated_normal',
 'uniform',
 'uniform_candidate_sampler']

通常我们可以忽略掉由__开头和结尾的函数(Python的特别对象)或者由_开头的函数(一般为内部函数)。通过其余成员的名字我们大致猜测出这个模块提供了各种随机数的生成方法,包括从均匀分布采样(uniform)、从正态分布采样(normal)、从泊松分布采样(poisson)等。

2.4.2 use of functions

想了解某个函数或者类的具体用法时,可以使用help函数。让我们以ones函数为例,查阅它的用法。更详细的信息,可以通过Tensorflow的API文档版本选择页,选择与自己环境中的 tensorflow 版本一致的 API 版本进行查询。

help(tf.ones)
Help on function ones in module tensorflow.python.ops.array_ops:

ones(shape, dtype=tf.float32, name=None)
    Creates a tensor with all elements set to 1.
    
    This operation returns a tensor of type `dtype` with shape `shape` and all
    elements set to 1.
    
    For example:
    
    ```python
    tf.ones([2, 3], tf.int32)  # [[1, 1, 1], [1, 1, 1]]
    ```
    
    Args:
      shape: A list of integers, a tuple of integers, or a 1-D `Tensor` of type
        `int32`.
      dtype: The type of an element in the resulting `Tensor`.
      name: A name for the operation (optional).
    
    Returns:
      A `Tensor` with all elements set to 1.

从文档信息我们了解到,ones函数会创建和输入tensor形状相同且元素为1的新tensor。我们可以验证一下:

tf.ones([2,3], tf.int32)
<tf.Tensor: id=2, shape=(2, 3), dtype=int32, numpy=
array([[1, 1, 1],
       [1, 1, 1]])>

在这里插入图片描述

发布了205 篇原创文章 · 获赞 655 · 访问量 53万+

猜你喜欢

转载自blog.csdn.net/qq_33414271/article/details/103593231