Nemo
Nemo
TensorFlow 介绍及使用
TensorFlow 介绍及使用

[TOC]

介绍

比较流行的框架有 TensorFlow,Keras,Pytorch 等,他们都有各自独特的特点。其中,TensorFlow 因为背靠谷歌 Google 这座大山,再加上庞大的开发者群体,更新和发版速度着实非常快。了解并掌握 TensorFlow 的使用,将使你在搭建深度学习模型时更加得心应手。

实验知识点

  • TensorFlow 介绍
  • TensorFlow 工作原理
  • 计算流图
  • 张量的类型
  • 启动会话
  • 常量生成
  • 梯度优化器
  • GPU 使用
  • 小批量梯度下降
  • 预训练模型

TensorFlow 介绍

TensorFlow 的特点:

  1. 高度灵活性:采用数据流图结构,只要计算可以表示为一个数据流图,就可以使用 TensorFlow。
  2. 可移植性: 在 CPU,GPU,服务器,移动端,云端,Docker 容器上都可以运行。
  3. 自动求微分: 在 TensorFlow 中,梯度的运算都将基于你输入的模型结构和目标函数自动完成。
  4. 多语言支持: 提供 Python,C++,Java,Go 接口。
  5. 优化计算资源:TensorFlow 允许用户将数据流图上的不同计算元素分配到用户不同设备上(CPU 内核,GPU 显卡),最大化利用你的硬件资源来进行深度学习运算。

TensorFlow 的技术栈图:

https://kanghaov-img-1256185664.file.myqcloud.com/2019/07/14/5d2b2b1b3ee34.png

最下面的是 TensorFlow 内核,底层 API 是通过 Python,C++,Java,Go 编写,中层仅提供了支持 Python 的主要的 3 类 API 接口,最上层是高度简略的 Estimator 估计器接口。当然,我们在了解时,将重点学习中层和顶层 API 使用。

TensorFlow 工作原理

数据流图(Data Flow Graphs):把一个完整的数值计算过程的重要步骤进行拆解,并形成一张有向图。图中,节点通常代表数学运算(如加减乘除),边表示节点之间的某种联系,它负责传输数据。

https://kanghaov-img-1256185664.file.myqcloud.com/2019/07/14/5d2b4b0a05b9a.png

这张数据流图反映的数值计算过程如下:

$$ result = b\times b – a\times c\times 5$$

根据数据流图的思想,我们可以把运算过程划分如下 4 个步骤:

$$ t_{1} =a\times c $$

$$ t{2} = 5\times t{1} $$

$$ t_{3} = b\times b $$

$$ result = t{3} – t{2} $$

疑问:那就是数据流图似乎是把简单的过程复杂化了呢?为什么可以一步到位的公式,硬是要拆开计算呢?

答:在 TensorFlow 中使用数据流图的目的,主要是为了将计算表示为独立的指令之间的依赖关系。数据流图可以为并行处理分布式计算带来优势。节点可以被分配到多个计算设备上,可以异步并行地执行操作。因为是有向图,所以只有等到之前的节点计算完成后,当前节点才能执行操作。

回到给出的数据流图,当输入 $a$ , $b$ , $c$ 的值后,模型就会按数据流图计算出结果。我们可以考虑把 $t{3}$ 的运算放在 CPU 上,把 $t{1}$ 和 $t{2}$ 的运算放在 GPU 上,最后再在 CPU 上得到结果,TensorFlow 就是按照这样的思想允许用户将数据流图的运算整合到不同设备上的。值得注意的是,无论如何分配 $t{2}$ 和 $t{1}$ 的运算设备, $t{2}$ 总是要等待 $t_{1}$ 运算结束才能开始运算。

通过数据流图来实现计算过程,可以尽可能地提升计算效率:

https://kanghaov-img-1256185664.file.myqcloud.com/2019/07/14/5d2b4d1d2d163.png

输入的数据流陆续到达不同的层,最终通过随机梯度下降 SGD 找到最优参数。

张量 Tensor 和会话 Session

在 TensorFlow 中,有 3 个重要的组件,它们分别是:

  • 张量(Tensor)
  • 会话(Session)
  • 图(Graph)

其中,张量 Tensor 是基本要素,也就是数据图 Graph 就是上面数据流图会话 Session 是运行数据流图的机制

TensorFlow 其实就是反映了张量在数据流图中的执行过程

张量 Tensor

张量的概念贯穿于物理学和数学中,并不那么浅显易懂。例如,下面有两种关于张量的定义(维基百科):

  • 通常定义张量的物理学或传统数学方法,是把张量看成一个多维数组,当变换坐标或变换基底时,其分量会按照一定规则进行变换,这些规则有两种:即协变或逆变转换。
  • 通常现代数学中的方法,是把张量定义成某个矢量空间或其对偶空间上的多重线性映射,这矢量空间在需要引入基底之前不固定任何坐标系统。例如协变矢量,可以描述为 1-形式,或者作为逆变矢量的对偶空间的元素。

简洁版介绍:

先前的介绍中,我们把 1 维的数组称之为向量,2 维的数组称之为矩阵。那么张量其实代表着更大的范围,你也可以把其看作是 n 维数组

现在重新描述向量和矩阵,就可以是:一阶张量向量二阶张量矩阵。当然,零阶张量也就是标量,而更重要的是 $n$ 阶张量,也就是 n 维数组

数学实例
0 标量(只有大小)
1 矢量(大小和方向)
2 矩阵(数据表)
3 3 阶张量(数据立体)
n n 阶张量(自行想象)

https://kanghaov-img-1256185664.file.myqcloud.com/2019/07/14/5d2b500736302.png

如果不严谨的讲,张量就是 n 维数组。前面提到的向量、矩阵,也是张量。

在 TensorFlow 中,每一个 Tensor 都具备两个基础属性:

  • 数据类型(默认:float32)
  • 形状

其中,数据类型如下表所示:

Tensor 类型 描述
tf.float32 32 位浮点数
tf.float64 64 位浮点数
tf.int64 64 位有符号整型
tf.int32 32 位有符号整型
tf.int16 16 位有符号整型
tf.int8 8 位有符号整型
tf.uint8 8 位无符号整型
tf.string 可变长度的字节数组
tf.bool 布尔型
tf.complex64 实数和虚数

另外,TensorFlow 通过三种符号约定来描述张量维度

  • 形状
  • 维数

三者之间的关系如下:

形状 维数 示例
[] 0 0-D 0 维张量。标量。
[$D_0$] 1 1-D 形状为 [5] 的 1 维张量。
[$D_0$, $D_1$] 2 2-D 形状为 [3, 4] 的 2 维张量。
[$D_0$, $D_1$, $D_2$] 3 3-D 形状为 [1, 4, 3] 的 3 维张量。
[$D_0$, $D1$, … $D{n-1}$] n n-D 形状为 [$D_0$, $D1$, … $D{n-1}$] 的张量。

根据不同的用途,TensorFlow 中的张量大致有 4 种类型,分别是:

  • tf.Variable:变量 Tensor,需要指定初始值,常用于定义可变参数。
  • tf.constant:常量 Tensor,需要指定初始值,定义不变化的张量。
  • tf.placeholder:占位 Tensor,不必指定初始值,可在运行时传入数值。
  • tf.SparseTensor:稀疏 Tensor,不常用。

tf.Variable 以外,其余类型张量的值不可变,这意味着在单一执行的情况下,张量只有一个值

我们可以通过传入数组来新建变量和常量类型的张量:

import tensorflow as tf
tf.Variable([1., 2., 3., 4.])

结果:

<tf.Variable 'Variable:0' shape=(4,) dtype=float32_ref>

再输入:

tf.constant([1., 2., 3., 4.])

结果:

<tf.Tensor 'Const:0' shape=(4,) dtype=float32>

这里其实输出了 Tensor 的属性,也就是包含有形状 shape 以及数据类型 dtype,而不能打印出Tensor的数据。看下面的内容。

会话 Session

如何得到 Tensor 的值?这里就要生硬地提出 TensorFlow 的一个特点,那就是变量 Tensor 要想输出值或者参与任何运算,都需要得到初始化,并通过会话 Session 机制完成。

也就是说,如果我们想得到上面变量 Tensor 的值,就需要这样做:

x = tf.Variable([1., 2., 3., 4.])

sess = tf.Session() # 建立会话
sess.run(x.initializer) # 初始化变量
sess.run(x) # 得到变量的值

输出:

array([1., 2., 3., 4.], dtype=float32)

对于常量 Tensor 是不需要初始化的,但是建立会话必不可少

x = tf.constant([1., 2., 3., 4.])

sess = tf.Session() # 建立会话
sess.run(x) # 得到变量的值

输出:

array([1., 2., 3., 4.], dtype=float32)

以下图的计算过程为例:

https://kanghaov-img-1256185664.file.myqcloud.com/2019/07/14/5d2b4b0a05b9a.png

使用tf如何实现这个过程:

a = tf.Variable(3, name="a")
b = tf.Variable(4, name="b")
c = tf.Variable(5, name='c')
f = b * b - a * c * 5
f

输出:

<tf.Tensor 'sub_2:0' shape=() dtype=int32>

出现这样的结果是因为没有对变量进行初始化,同时,为了对已有的数据流图进行赋值运算,需要建立一个TensorFlow会话,在这个会话里初始化变量和运算出f的结果。

本质上,一个会话才能将运算操作分配到像 CPU 和 GPU 这样的硬件设备然后开始执行这个数据流图,并且存储所有的变量数值。

sess = tf.Session()
sess.run(a.initializer)
sess.run(b.initializer)
sess.run(c.initializer)
sess.run(f)

输出:

-59

注意,在执行完运算后,需要关闭会话,才能把所占空间释放掉。

sess.close()

在上述的过程中我们通过调用 3 次 sess.run() 去初始化所有变量,另一次是通过 sess.run() 运算出 f 的值,这样会显得有些累赘。为了方便,我们常用的方法是设置一个初始节点,并将全部变量进行初始化。同时,**通过 eval() 操作来对结果进行运算,也不用单独 sess.close()**

init = tf.global_variables_initializer()

with tf.Session() as sess:
    init.run()
    result = f.eval()
result

输出:

-59

在这里,变量全部定义为实数常量,为什么要定义成变量张量而不是常量张量呢?常量与变量的区别是,在未来的运算过程中,变量 Tensor 的数值是可以变化的,如利用梯度下降更新权值。但是,常量 Tensor 则保持不变,如训练数据集。

其实,这里如何定义都是没影响的,只是变量张量通常用来定义需要更新的参数,这里不需要更新也无所谓了。当然,我们完全可以将其改为常量张量进行运算。

a = tf.constant(3, name="a")    
b = tf.constant(4, name="b")
c = tf.constant(5, name='c')
f = b*b - a * c * 5

with tf.Session() as sess:
    result = f.eval()
result

输出:

-59

因为常量不需要初始化,所以还少了一些代码。

最后还有一种常用的类型张量,称之为占位符 Tensor tf.placeholder。在前面我们看到了常量 Tensor 和变量 Tensor 在创建的时候都会被赋予一个值(对变量来讲是初始值)。但对于一些张量的创建,我们是不知道它的数值的,甚至连该张量的维度都不是很清晰,但可以肯定的是,我们在之后的运算中会对这个张量调用其他的有确定数值的数据來赋值,那么我们可以利用占位符 tf.placeholder 来创建这个张量。

例如:

import numpy as np

x = tf.placeholder(tf.float32, shape=(3, 3)) # 创建占位符张量
y = tf.matmul(x, x) # 乘法计算

with tf.Session() as sess:
    x_test = np.random.rand(3, 3) # 生成 numpy 数组作为测试数据
    result = sess.run(y, feed_dict={x: x_test}) # 通过 feed_dict 把测试数据传给占位符张量
result

输出:

array([[1.244235  , 0.5561632 , 0.9367316 ],
       [0.720096  , 0.3721605 , 0.7985977 ],
       [0.66261744, 0.26277015, 0.5142922 ]], dtype=float32)

占位符 Tensor 同样是一种非常重要的张量类型,在构建模型的时候经常会用到它。比如对于一些形式参数,占位符 Tensor 用于定义过程,而真正执行的时候再赋给具体的值

常用类和方法

作为深度学习第一开源框架,TensorFlow 的特点在于快速构建深度学习网络。而快速构建的要点在于,TensorFlow 提供了大量封装好的函数和方法。

在构建深度神经网络时,TensorFlow 可以说提供了你一切想要的组件,从不同形状的张量、激活函数、神经网络层,到优化器、数据集等,一应俱全。所以,接下来,我们过一遍 TensorFlow 中常用到的类和方法。这一小节的目的在于,让你对 TensorFlow 整体更加熟悉。

常量、序列和随机值

上面已经介绍了构建常见的常量及变量张量的方法了,这里再列举几个新建特殊常量张量的方法:

  • tf.zeros:新建指定形状且全为 0 的常量 Tensor
  • tf.zeros_like:参考某种形状,新建全为 0 的常量 Tensor
  • tf.ones:新建指定形状且全为 1 的常量 Tensor
  • tf.ones_like:参考某种形状,新建全为 1 的常量 Tensor
  • tf.fill:新建一个指定形状且全为某个标量值的常量 Tensor

为了便于查看输出值,我们先创建 Session:

sess = tf.Session()

c = tf.zeros([3, 3]) # 3x3 全为 0 的常量 Tensor
sess.run(c)

输出:

array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]], dtype=float32)

与 c 形状一致全为 1 的常量 Tensor:

sess.run(tf.ones_like(c)) # 与 c 形状一致全为 1 的常量 Tensor

输出:

array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]], dtype=float32)

2×3 全为 6 的常量 Tensor:

sess.run(tf.fill([2, 3], 6)) # 2x3 全为 6 的常量 Tensor

输出:

array([[6, 6, 6],
       [6, 6, 6]])

除此之外,我们还可以创建一些序列,例如:

  • tf.linspace:创建一个等间隔序列。
  • tf.range:创建一个数字序列。

tf.linspace:

sess.run(tf.linspace(1.0, 10.0, 5, name="linspace"))

输出:

array([ 1.  ,  3.25,  5.5 ,  7.75, 10.  ], dtype=float32)

tf.range:

start = 1
limit = 10
delta = 2
sess.run(tf.range(start, limit, delta))

输出:

array([1, 3, 5, 7, 9])

当然,创建随机张量也是日常必不可少的操作,例如:

  • tf.random_normal:正态分布中输出随机值。
  • tf.truncated_normal:从截断的正态分布中输出随机值。
  • tf.random_uniform:从均匀分布中输出随机值。
  • tf.random_shuffle:沿着指定维度打乱张量。
  • tf.random_crop:随机将张量裁剪为给定的大小。
  • tf.multinomial:从多项分布中输出随机值。
  • tf.set_random_seed:设定随机数种子。
tf.set_random_seed(10)

sess.run(tf.random_normal(
    shape=[5, 5],
    mean=2.0,
    stddev=1.0,
    dtype=tf.float32
))

输出:

array([[1.1390473, 4.0065084, 2.7612598, 2.106696 , 3.3083415],
       [1.2797012, 1.6688279, 3.1097832, 2.3501904, 1.0085891],
       [2.5511127, 2.4938307, 1.8987106, 1.7954186, 2.5273113],
       [0.9819553, 1.8957413, 2.3792088, 1.3320477, 1.8846449],
       [3.9741006, 2.2700171, 4.2264915, 1.7159973, 2.207233 ]],
      dtype=float32)

数学计算

TensorFlow 中提供的数学计算,包括线性代数计算方面的方法也是应有尽有,十分丰富。下面,我们只是列举几个示例。

a = tf.constant([1., 2., 3., 4., 5., 6.], shape=[2, 3])
b = tf.constant([7., 8., 9., 10., 11., 12.], shape=[3, 2])

c = tf.matmul(a, b) # 矩阵乘法
sess.run(c)

输出:

array([[ 58.,  64.],
       [139., 154.]], dtype=float32)

转置矩阵:

sess.run(tf.transpose(c)) # 转置矩阵

输出:

array([[ 58., 139.],
       [ 64., 154.]], dtype=float32)

求逆矩阵:

sess.run(tf.matrix_inverse(c))

输出:

array([[ 4.277741 , -1.7777623],
       [-3.8610775,  1.6110971]], dtype=float32)

更多方法,请查阅 官方文档

神经网络层

TensorFlow 作为最出色的深度学习开源框架,主要有 2 点优势。

  1. 第一是基于计算图的数值计算过程,最终目的是提升计算速度。
  2. 其次就是对各种常用神经网络层进行封装,提高搭建模型的速度。

另外像创建张量(n 维数组),执行各类数学计算。我们之前接触到的 NumPy 也可以完成,但是上面两点只有使用 TensorFlow 这类专门用于深度学习的开源框架才能做到。所以,下面说一下 TensorFlow 内置的各种神经网络层。这些类基本都在 tf.nn 下面。

首先是激活函数:

  • tf.nn.relu
  • tf.nn.relu6
  • tf.nn.crelu
  • tf.nn.elu
  • tf.nn.selu
  • tf.nn.softplus
  • tf.nn.softsign
  • tf.nn.dropout
  • tf.nn.bias_add
  • tf.sigmoid
  • tf.tanh

另外,当我们搭建卷积神经网络的时候,会用到大量的卷积层:

  • tf.nn.convolution
  • tf.nn.conv2d
  • tf.nn.depthwise_conv2d
  • tf.nn.depthwise_conv2d_native
  • tf.nn.separable_conv2d
  • tf.nn.atrous_conv2d
  • tf.nn.atrous_conv2d_transpose
  • tf.nn.conv2d_transpose
  • tf.nn.conv1d
  • tf.nn.conv3d
  • tf.nn.conv3d_transpose
  • tf.nn.conv2d_backprop_filter
  • tf.nn.conv2d_backprop_input
  • tf.nn.conv3d_backprop_filter_v2
  • tf.nn.depthwise_conv2d_native_backprop_filter
  • tf.nn.depthwise_conv2d_native_backprop_input

用 TensorFlow 实现线性回归

尝试用 TensorFlow 去实现一个线性回归,目的主要是熟悉 TensorFlow 搭建模型的整个流程,以及诸如 Placeholder,Constant,Optimizer 等重要概念。

我们尝试对加州房价进行预测。我们利用 TensorFlow 搭建了一个线性回归模型的数据流图,并且利用最小二乘法计算出相应的权值 weight(代码里用 theta 表示)。

首先,我们加载示例数据集。这里使用 sklearn 里自带的数据集。

import numpy as np
from sklearn.datasets import fetch_california_housing # sklearn 自带数据集

housing = fetch_california_housing()   # 数据集,其中 housing.data 表示特征数据,housing.target 是目标数据
m, n = housing.data.shape  # m, n 存储特征数据的维度,因为最小二乘法里我们需要对特征数据补上一列元素均为1的向量
housing_feature = np.append(housing.data, np.ones((m,1)), axis=1)   # 补 1 操作,注意这里的np.ones((m,1))格式

housing_feature

易出错:np.ones((m,1)),容易写错成np.ones(m,1)

输出:

array([[   8.3252    ,   41.        ,    6.98412698, ...,   37.88      ,
        -122.23      ,    1.        ],
       [   8.3014    ,   21.        ,    6.23813708, ...,   37.86      ,
        -122.22      ,    1.        ],
       [   7.2574    ,   52.        ,    8.28813559, ...,   37.85      ,
        -122.24      ,    1.        ],
       ...,
       [   1.7       ,   17.        ,    5.20554273, ...,   39.43      ,
        -121.22      ,    1.        ],
       [   1.8672    ,   18.        ,    5.32951289, ...,   39.43      ,
        -121.32      ,    1.        ],
       [   2.3886    ,   16.        ,    5.25471698, ...,   39.37      ,
        -121.24      ,    1.        ]])

TensorFlow 实现最小二乘法

接下来,我们使用 TensorFlow 实现最小二乘法,这里使用到前面实验中学过的最小二乘法矩阵推导公式。

# 定义 X 为一个常数张量,值为 housing.feature 的值
X = tf.constant(housing_feature, dtype=tf.float32, name='X')

# reshape(-1,1)将目标数据设定为一个列向量,同样地,我们定义 y 为一个常数张量
Y = tf.constant(housing.target.reshape(-1, 1), dtype=tf.float32, name='Y')

# X 的转置
XT = tf.transpose(X)

# tf.matmul 是在 tensorflow 里的矩阵乘法,tf.matrix_inverse 求出矩阵的逆矩阵
theta = tf.matmul(tf.matmul(tf.matrix_inverse(tf.matmul(XT, X)), XT), Y)

theta

结果:

<tf.Tensor 'MatMul_4:0' shape=(9, 1) dtype=float32>

上面说过,要执行运算必须通过新建会话来完成:

with tf.Session() as sess:      # 建立会话开始运算 theta 的值
    theta_val = theta.eval()  # 由于数据流图里没有变量(X,Y,XT 均为常数张量),这里不需要初始化变量操作。
theta_val

输出:

array([[ 4.3538418e-01],
       [ 9.3739806e-03],
       [-1.1054278e-01],
       [ 6.6589427e-01],
       [-4.1245030e-06],
       [-3.7869136e-03],
       [-4.2115396e-01],
       [-4.3428165e-01],
       [-3.6921303e+01]], dtype=float32)

TensorFlow 手动实现梯度下降

在梯度下降算法里,我们需要初始化 theta 的值,然后在每一次迭代过程中,通过梯度下降的原则对 theta 值进行更新,所以 theta 应该是一个变量。为了使梯度下降运行起来更加顺畅,我们先对数据进行归一化操作。这里直接使用 sklearn 提供的 StandardScaler() 方法。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaled_housing_data = scaler.fit_transform(housing.data)
scaled_housing_data_plus_bias = np.append(scaled_housing_data,np.ones((m,1)),axis=1)

上面提到,创建一个常量张量的时候,我们有很多种方式。常用的有 tf.ones(shape)tf.zeros(shape) 可以分别创造出一个 shape 大小的全 1 或全 0 矩阵常量。回顾梯度下降我们知道随机性可能是一个比较好的选择,这里我们利用 tf.random_uniform 来创建一个均匀分布常量。

当然,这里需要将常量转换为变量使用。我们接下来实现梯度下降的过程:

n_epochs = 1000 # 指定迭代次数
learning_rate = 0.01 # 指定学习率

X = tf.constant(scaled_housing_data_plus_bias, dtype=tf.float32, name="X")
Y = tf.constant(housing.target.reshape(-1, 1), dtype=tf.float32, name="Y")

# theta 初始化为一个(n+1)*1 大小,元素均匀分布于 -1 到 1 的矩阵
theta = tf.Variable(tf.random_uniform([n + 1, 1], -1.0, 1.0, seed=42), name="theta")
Y_pred = tf.matmul(X, theta, name="predictions")  # 对于每一次的 theta,y 的预测值
error = Y_pred - Y # 计算残差

# 平方误差,注意 TensorFlow 中计算矩阵元素的期望是 tf.reduce_mean
mse = tf.reduce_mean(tf.square(error), name="mse")
gradients = 2/m * tf.matmul(tf.transpose(X), error)  # 手动算出梯度
training_op = tf.assign(theta, theta - learning_rate * gradients)  # 每一次迭代的操作, 将 theta 的值重新赋值

接下来,我们使用 tf.global_variables_initializer() 初始化全局变量(不再需要一个一个初始化),并启动 Session 构建计算图:

init = tf.global_variables_initializer() # 初始化全局变量

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        if epoch % 100 == 0:
            print("Epoch ", epoch, "MSE = ", mse.eval())
        sess.run(training_op)
    best_theta = theta.eval()

best_theta

输出:

Epoch 0 MSE = 8.393569
Epoch 100 MSE = 0.6255079
Epoch 200 MSE = 0.5355903
Epoch 300 MSE = 0.5314589
Epoch 400 MSE = 0.529454
Epoch 500 MSE = 0.52802384
Epoch 600 MSE = 0.5269924
Epoch 700 MSE = 0.52624834
Epoch 800 MSE = 0.52571154
Epoch 900 MSE = 0.5253243
array([[ 8.1519800e-01],
       [ 1.2665275e-01],
       [-2.1844473e-01],
       [ 2.5804031e-01],
       [-1.4415225e-03],
       [-3.9687146e-02],
       [-8.5583121e-01],
       [-8.2384837e-01],
       [ 2.0685523e+00]], dtype=float32)

注意,你可能会发现这里结果跟最小二乘法结果相差很大。这是因为我们对原数据集做了归一化的操作,这里的 theta 将是对归一化后数据的较优权值。

利用 TensorFlow 优化器实现梯度下降

上文在介绍 TensorFlow 时,特意提到了 TensorFlow 的一大优势,是它能基于所搭建的模型和目标函数自动地计算出梯度。这其实是通过 TensorFlow 里内置的各种优化器(Optimizer)来实现的,比如:

  • tf.train.GradientDescentOptimizer:梯度下降 Optimizer
  • tf.train.MomentumOptimizer:使用 Momentum 算法的 Optimizer
  • tf.train.RMSPropOptimizer:使用 RMSProp 算法的 Optimizer
  • tf.train.AdamOptimizer:使用 Adam 算法的 Optimizer

当然,还有更多的优化器,可以通过 此页面 查看。简单来讲,这些优化器都是以梯度优化为思想,通过学习率作为步长来逐步更新变量的值。

下面,我们使用 TensorFlow 提供的梯度下降优化器来完成实验,这个过程会异常简单:

learning_rate = 0.01 # 指定学习率

optimizer = tf.train.GradientDescentOptimizer(learning_rate = learning_rate)  # 选择梯度优化器,设定上文同样的学习率
training_op = optimizer.minimize(mse)  # 设定优化器优化的目标函数

init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        if epoch % 100 == 0:
            print("Epoch", epoch, "MSE =", mse.eval())
        sess.run(training_op)  # 给定模型和目标函数,TensorFlow 自动计算出梯度更新变量值
    best_theta = theta.eval()

best_theta

输出:

Epoch 0 MSE = 8.393569
Epoch 100 MSE = 0.6255079
Epoch 200 MSE = 0.5355903
Epoch 300 MSE = 0.5314589
Epoch 400 MSE = 0.529454
Epoch 500 MSE = 0.5280239
Epoch 600 MSE = 0.52699244
Epoch 700 MSE = 0.52624834
Epoch 800 MSE = 0.52571154
Epoch 900 MSE = 0.5253243
array([[ 8.1519800e-01],
       [ 1.2665275e-01],
       [-2.1844476e-01],
       [ 2.5804034e-01],
       [-1.4415244e-03],
       [-3.9687142e-02],
       [-8.5583127e-01],
       [-8.2384843e-01],
       [ 2.0685523e+00]], dtype=float32)

不出意外,这和我们手动实现的梯度下降计算结果一模一样。

实际上,大量实践表明,Adam 优化器在梯度运算时,尤其是在深度神经网络时效果较佳,所以建议大家以后在做梯度下降时,默认先尝试 Adam 优化器。除非收敛效果不佳时,才考虑更换其他优化器。我们再试一次:

optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)  # 选择 Adam 优化器,设定优化器的学习率
training_op = optimizer.minimize(mse)  # 优化器优化的目标函数

init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        if epoch % 100 == 0:
            print("Epoch", epoch, "MSE =", mse.eval())
        sess.run(training_op)  # 给定模型和目标函数,TensorFlow 自动计算出梯度更新变量值
    best_theta = theta.eval()

best_theta

输出:

Epoch 0 MSE = 8.393569
Epoch 100 MSE = 1.2885945
Epoch 200 MSE = 0.5772578
Epoch 300 MSE = 0.5361455
Epoch 400 MSE = 0.5313901
Epoch 500 MSE = 0.52884483
Epoch 600 MSE = 0.5270801
Epoch 700 MSE = 0.52590793
Epoch 800 MSE = 0.525181
Epoch 900 MSE = 0.5247598
array([[ 0.8101229 ],
       [ 0.11859997],
       [-0.22446814],
       [ 0.269857  ],
       [-0.00427343],
       [-0.0388963 ],
       [-0.9170078 ],
       [-0.88524455],
       [ 2.0685563 ]], dtype=float32)

可以看到,Adam 最终迭代的 MSE 值会更小一些,也代表效果稍好一些。

关于更多优化器的对比,大家可以阅读 [An overview of gradient descent optimization algorithms]

利用 TensorFlow 实现 Mini Batch 训练

深度学习是依赖于非常大的数据量,所运用的强力机器学习方法。如果数据量非常大,再加上神经网络很深的话,通常会花费的训练时间从数小时到数个月不等,持续时间非常长。

于是,在实践中往往会使用一种叫 Mini Batch 的方法,也就是将整个数据分成一些小批次放进模型里进行训练。

上面的梯度下降优化过程中,我们每一次更新 theta 的值,则需要遍历所有的样本数据集。如果使用 Mini Batch 的方法,就只需要将这部分小批量数据作为用于本次更新的数据集,遍历这个小批量就可以使 theta 有一次更新。

实践表明,小批量梯度下降训练速度更快,最终结果也不逊于以往的传统梯度下降。所以,接下来我们就用 Mini Batch 的方法重新训练。

训练之前,我们需要特别注意一点。前面的实验中,我们直接将数据定为变量传入模型,而由于 Mini Batch 会不断地传入每一个 Batch,所以此时就需要用到 Placeholder 占位符张量了。使用 Placeholder 时,可以通过 feed_dict 将占位符予以赋值参与运算, 在调用结束后,填充数据就会消失。

n_epochs = 10
learning_rate = 0.01

# 将 X 创建为占位符,类型为 32 位浮点型,大小由于维度未知可以先记为 None,以后由 feed_dict 的数据决定
X = tf.placeholder(tf.float32, shape=(None, n + 1), name="X")
Y = tf.placeholder(tf.float32, shape=(None, 1), name="Y")

# 初始化权重以及定义 MSE
theta = tf.Variable(tf.random_uniform([n + 1, 1], -1.0, 1.0, seed=42), name="theta")
Y_pred = tf.matmul(X, theta, name="predictions")
error = Y_pred - Y
mse = tf.reduce_mean(tf.square(error), name="mse")

# 使用 Adam 优化器
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(mse)

接下来,我们需要定义一个取小批量的函数,以基于某个随机种子构建 feed_dict 的小批量数据。

# 设定整个数据集分为多少小批量
batch_size = 100
n_batches = int(np.ceil(m / batch_size))

# 定义一个取小批量的函数
def fetch_batch(epoch, batch_index, batch_size):
    # 每一次基于某个随机种子构建 feed_dict 的小批量数据
    np.random.seed(epoch * n_batches + batch_index)
    # 随机产生批量大小的索引值向量来取出批量数据
    indices = np.random.randint(m, size=batch_size)
    X_batch = scaled_housing_data_plus_bias[indices]
    Y_batch = housing.target.reshape(-1, 1)[indices]
    return X_batch, Y_batch

# 初始化变量并构建计算图
init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        for batch_index in range(n_batches):
            # 每一次 X_batch,Y_batch 通过 fetch_batch 函数从原数据集中取出
            X_batch, Y_batch = fetch_batch(epoch, batch_index, batch_size)
            sess.run(training_op, feed_dict={X: X_batch, Y: Y_batch})
    best_theta = theta.eval()

best_theta

输出:

array([[ 0.8281455 ],
       [ 0.12619452],
       [-0.26218927],
       [ 0.32747453],
       [ 0.00839825],
       [-0.03493264],
       [-0.9310693 ],
       [-0.8630609 ],
       [ 2.0594535 ]], dtype=float32)

存储或重启模型

由于深度神经网络训练艰难,对已经训练完成的网络进行进一步的设计或者迁移学习能极大提高训练效率。同时,我们经常称这些保持下来的模型为预训练模型。

接下来,我们就学习如何保存模型,其实非常简单,只需要在适当的位置添加几行代码即可:

import os

# 获取当前 notebook 所在目录,将预训练模型保持在该目录下方,也可以使用绝对路径
path = os.path.abspath(os.curdir)
saver = tf.train.Saver() # 存储预训练模型

init = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        save_path = saver.save(sess, path + '/model.ckpt')  # 在训练过程中存储模型参数
        for batch_index in range(n_batches):
            X_batch, Y_batch = fetch_batch(epoch, batch_index, batch_size)
            sess.run(training_op, feed_dict={X: X_batch, Y: Y_batch})
    best_theta = theta.eval()
    save_path = saver.save(sess, path+"/model.ckpt")  # 存储最终模型参数
    print("done.")

best_theta

输出:

array([[ 0.8281455 ],
       [ 0.12619452],
       [-0.26218927],
       [ 0.32747453],
       [ 0.00839825],
       [-0.03493264],
       [-0.9310693 ],
       [-0.8630609 ],
       [ 2.0594535 ]], dtype=float32)

重启这个模型非常简单,我们只需要使用 saver.restore() 即可:

with tf.Session() as sess:
    saver.restore(sess, path+"/model.ckpt")
    best_theta_restored = theta.eval()

best_theta_restored

输出:

array([[ 0.8281455 ],
       [ 0.12619452],
       [-0.26218927],
       [ 0.32747453],
       [ 0.00839825],
       [-0.03493264],
       [-0.9310693 ],
       [-0.8630609 ],
       [ 2.0594535 ]], dtype=float32)

使用 GPU 训练模型

相比于适合逻辑运算的 CPU,GPU 往往拥有数千个核心,特别适合于并行数值计算。

TensorFlow 支持 CPU 和 GPU 这两种设备。它们均用 strings 表示。例如:

  • "/cpu:0":机器的 CPU。
  • "/device:GPU:0":机器的 GPU(如果有一个)。
  • "/device:GPU:1":机器的第二个 GPU(以此类推)。

如果 TensorFlow 指令中兼有 CPU 和 GPU 实现,当该指令分配到设备时,GPU 设备有优先权。

with tf.device('/cpu:0'):
    a = tf.Variable([[1., 2.], [3., 4.]])
    b = tf.Variable([[5., 6.], [7., 8.]])

c = tf.matmul(a, b)
c

总结一下 Tensorflow 的运算设备配置原则:

  1. 如果该节点在之前数据流图运算中已经置于了一个设备,那么它留在这个设备进行运算;
  2. 如果用户将这个节点置换到另一个设备,那么该节点在置换后的设备进行运算;
  3. 都不是的话,它默认在第一个 GPU 上运算,如果没有 GPU 的话则在 CPU 上运算。
赞赏
Nemo版权所有丨如未注明,均为原创丨本网站采用BY-NC-SA协议进行授权,转载请注明转自:https://kanghaov.com/223.html
https://secure.gravatar.com/avatar/9fd8359b8faa6f7789f9623ba6041e4a?s=256&d=monsterid&r=g

kanghaov

文章作者

推荐文章

发表评论

textsms
account_circle
email

Nemo

TensorFlow 介绍及使用
[TOC] 介绍 比较流行的框架有 TensorFlow,Keras,Pytorch 等,他们都有各自独特的特点。其中,TensorFlow 因为背靠谷歌 Google 这座大山,再加上庞大的开发者群体,更新和发版速度着实非…
扫描二维码继续阅读
2019-07-15