简明机器学习教程(二)—— 实践:进入 Tensorflow 世界

经过了一年的休整,终于博客也要恢复原先坑着的系列了,《简明机器学习教程》也会恢复更新。说实在的,第二篇的原稿我其实在第一篇之后一星期就写出来了,但是后来因为原稿遗失与学业繁忙就一直拖了下来。历经一年,我对机器学习与这系列教程又有了些新的思考,所以我决定做出些许调整。首先,本系列不再单独分理论、实践篇,而是采用交织在一起的形式。其次,将 matlab 更换为 tensorflow (python)。教程的定位依旧是面向初学者,所以会加入大篇幅的前置介绍。这篇就是为了之后内容而对 tensorflow 进行先行的介绍。

安装(Windows)

安装 CUDA 和 cuDNN

如果有支持 CUDA 的显卡(仿佛听到农企在哭泣),那建议安装 CUDA。这样 tensorflow 就可以调用 GPU 而不是 CPU 进行计算,这会大大提升计算效率。不过并不是所有显卡都可以适用 tensorflow,一些算力过差的显卡依旧是不能使用的。这里牙膏厂采用了运算能力(capability)来衡量显卡的算力,在这里可以查看这个表格,tensorflow 官方要求 capability 必须大于 3.0。如果不满足这个条件,建议跳过安装 CUDA,直接安装 cpu 版本的 tensorflow。当然解决方案也是有的,参见 StackOverflow,不过需要自己编译 tensorflow。由于当前版本的 tensorflow 仅支持 CUDA9.0,所以只能到官网下载 9.0 版本。选择版本之后可以下载 network 或 local,这里建议选择 local(network 老是提示下载失败)。安装成功后,在命令行输入 nvcc -V 查看安装的版本,若有图示的信息则说明安装成功。

cuDNN 可以在此处下载,不过需要注意的是,只有登录才能下载。选择适合 CUDA9.0 的版本即可。下载后解压至 “安装路径 \NVIDIA GPU Computing Toolkit\CUDA\v9.0” 即可。

安装 Anaconda

Anaconda 集成了大量有关科学计算的包,而且自带了个非常棒的开发环境。当然,安装 tensorflow 时,Anaconda 并不是必要的,但是还是很推荐安装。在官网就可以下载其安装包,如果无法下载或下载失败,也可以选择清华的镜像。安装完之后,打开 Anaconda Navigator 就可以看到 jupyter notebook 了。这里建议顺手装个 jupyterlab,虽然用的人不是很多,但是 jupyterlab 极大的扩充了 jupyter notebook 的功能。

然后我们来做下准备工作,首先创建一个开发环境,并激活:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
conda create -n tensorflow pip python=3.5
activate tensorflow
conda create -n tensorflow pip python=3.5 activate tensorflow
conda create -n tensorflow pip python=3.5
activate tensorflow

如果左侧出现 “(tensorflow)” 字样,则说明已经切换至此环境了。

安装 Tensorflow

终于来到重头戏了。对于安装了 Anaconda 的情况,装了 CUDA 的话执行:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
pip install --ignore-installed --upgrade tensorflow-gpu
pip install --ignore-installed --upgrade tensorflow-gpu
pip install --ignore-installed --upgrade tensorflow-gpu

注意要在刚刚创建的环境下执行。(左侧应该有”(tensorflow)”)没安装 CUDA 则需要执行:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
pip install --ignore-installed --upgrade tensorflow
pip install --ignore-installed --upgrade tensorflow
pip install --ignore-installed --upgrade tensorflow

对于没安装 Anaconda 的情形,若安装了 CUDA 就执行:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
pip3 install --upgrade tensorflow-gpu
pip3 install --upgrade tensorflow-gpu
pip3 install --upgrade tensorflow-gpu

若没有安装则执行:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
pip3 install --upgrade tensorflow
pip3 install --upgrade tensorflow
pip3 install --upgrade tensorflow

至此为止,我们就完成了 tensorflow 的安装。

验证安装

打开 jupyter notebook 或者直接在 shell 键入 python,然后依此输入以下命令:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
>>> print(sess.run(hello))
>>> import tensorflow as tf >>> hello = tf.constant('Hello, TensorFlow!') >>> sess = tf.Session() >>> print(sess.run(hello))
>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
>>> print(sess.run(hello))

若能成功打印 “Hello, TensorFlow!” 则说明安装正常。如果报错,请看官网给出的常见问题

安装(Linux)

这部分先坑着,过段时间没那么懒的时候写。

简介

Tensorflow 与其说是一个机器学习库,更应该看作其官网宣称的数值计算库。当然,机器学习才是其设计用途。Tensorflow 最核心的原理与思想就是数据流图。数据流图是一张图(graph),这里的图指的是图论里的图。一张图通常包含了一些节点和连接这些节点的边,而数据流图就是这样一张图,进一步讲,是一张有向图(directed graph)。每个结点就代表了一个运算操作(operation,比如加减乘除),而数据就在途中定向的 “走动”。例如,我们把 (1+3)*2 转换为数据流图,那么它会长这样:

可以看到,1、3 两个数字先 “流” 向了 “Add(加)” 这个结点,然后和 2 “流” 向了 “Multiply(乘)” 这个结点。这样我们应该能更清楚的理解所谓的结点,每个结点都代表了处理若干数据的过程,它可以是函数或若干个步骤的计算。同时,这些结点也会给出一个 “结果”。而这些 “1”、“2”、“3” 的标量,与矢量、矩阵之类的数据都统称为张量(Tensor)。因为这些张量在图中由一个结点 “流(flow)” 向另一个结点,所以才取名为 TensorFlow。下面这张官方给出的动图就很能说明这个性质:

#使用数据流图的优点

张量

在数学中,有很多不同形式的量,比如标量(数量)、矢量(向量)、矩阵等。这些量都具有不同的维数,比如标量是 0 维的,矢量是 1 维的,矩阵是 2 维的。在 tensorflow 中,这些量都算张量,而维数就是它们的阶(rank,和矩阵的阶不同)。而如矢量、矩阵这类 1 阶以上的张量,它们还有不同的形状。比如:[123456] \begin{bmatrix}1 & 2\\ 3 & 4\\ 5 & 6\end{bmatrix}。这是一个 3 行 2 列的矩阵,而 (3,2) 就是它的形状。通过将所有数据都统一为具一定形状的张量,数据流图才得以个简单的结构。

开始

我们先引入 tensorflow。之后的代码中,我们将使用别名 tf 来指代 tensorflow。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import tensorflow as tf
import tensorflow as tf
import tensorflow as tf

从张量开始

之前已经介绍了张量,那我们就来看看张量在 tensorflow 中的具体实现。“在编写 TensorFlow 程序时,操控和传递的主要目标是 tf.Tensor。” 而 tf.Tensor 具有数据类型和形状两个类型,我们先来看数据类型。到目前为止,tensorflow 支持的数据类型如下表:

#tensorflow 的数据类型

然后我们来看看张量的阶:

[table width=”95%” th=”1″]

数学实例

o
标量(只有大小)

1
矢量(大小和方向)

2
矩阵(数据表)

3
3 阶张量(数据立体)

n
n 阶张量(自行想象)
[/table]

由于之前的介绍已经简单的讲解了阶的定义,那我们就直接来看如何创建张量。以下是 0-2 阶张量的一些创建示例:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 0 阶张量,标量
t_r0 = tf.constant(1, tf.int16)
t_s_r0 = tf.constant("Elephant", tf.string)
# 1 阶张量,矢量
t_r1 = tf.constant([1, 2, 3], tf.int16)
# 2 阶张量,矩阵
t_r2 = tf.constant([ [1, 2], [3, 4] ], tf.int32)
# 0 阶张量,标量 t_r0 = tf.constant (1, tf.int16) t_s_r0 = tf.constant ("Elephant", tf.string) # 1 阶张量,矢量 t_r1 = tf.constant ([1, 2, 3], tf.int16) # 2 阶张量,矩阵 t_r2 = tf.constant ([ [1, 2], [3, 4] ], tf.int32)
# 0阶张量,标量
t_r0 = tf.constant(1, tf.int16)
t_s_r0 = tf.constant("Elephant", tf.string)

# 1阶张量,矢量
t_r1 = tf.constant([1, 2, 3], tf.int16)

# 2阶张量,矩阵
t_r2 = tf.constant([ [1, 2], [3, 4] ], tf.int32)

同时,tensorflow 还提供了一些自建函数以供张量创建:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 2 阶张量,2x3 矩阵,元素皆为 0
t_zero = tf.zeros((2, 3))
# 2 阶张量,3x3 矩阵,元素为 [0,1) 间均匀分布
t_rand = tf.random_uniform((3, 3), minval=0, maxval=1)
# 2 阶张量,2x3 矩阵,元素皆为 0 t_zero = tf.zeros ((2, 3)) # 2 阶张量,3x3 矩阵,元素为 [0,1) 间均匀分布 t_rand = tf.random_uniform ((3, 3), minval=0, maxval=1)
# 2阶张量,2x3矩阵,元素皆为0
t_zero = tf.zeros((2, 3))

# 2阶张量,3x3矩阵,元素为[0,1)间均匀分布
t_rand = tf.random_uniform((3, 3), minval=0, maxval=1)

这些 (2, 3)、(3, 3) 就是这些张量的形状了。其实,创建张量的方法也远不止这些。事实上,Python 原生类型、NumPy 数组都可以直接传入 tensorflow 指令(tensorflow operation,下称 “指令”)。并且在传入时也会被自动转化为对应的张量。

数据流图

还记得上面例子中的数据流图吗?本节我们就将学习如何创建这样一个数据流图。我们先来解析下这张数据流图的代码。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
a = tf.constant(1, name="a")
b = tf.constant(3, name="b")
c = tf.constant(2, name="c")
op_add = tf.add(a, b, name="Add")
op_mul = tf.multiply(op_add, c, name="Multiply")
a = tf.constant(1, name="a") b = tf.constant(3, name="b") c = tf.constant(2, name="c") op_add = tf.add(a, b, name="Add") op_mul = tf.multiply(op_add, c, name="Multiply")
a = tf.constant(1, name="a")
b = tf.constant(3, name="b")
c = tf.constant(2, name="c")

op_add = tf.add(a, b, name="Add")
op_mul = tf.multiply(op_add, c, name="Multiply")

 

a、b、c 是常量张量,op_add、op_mul 是两个算符,对应于图中的 Add、Multiply 结点。而 tf.add 与 tf.multiply 就是构造这两个结点的函数。等等,op_add 如果是指令,那为什么能直接传给 tf.multiply 呢?我们不妨试一试打印 type (op_add),结果出人意料的是 “<class ‘tensorflow.python.framework.ops.Tensor’>”。没错,op_add 和 op_mul 都是张量!还记得那句话吗?“在编写 TensorFlow 程序时,操控和传递的主要目标是 tf.Tensor。” 这些创建结点的函数并不直接返回结点本身,而是返回结点计算结果的张量,以便进行接下来的运算。得益于 python 支持算符重载的语言特性,我们可以把上面的代码改的更加简便。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
op_add = a+b
op_mul = op_add*c
op_add = a+b op_mul = op_add*c
op_add = a+b
op_mul = op_add*c

当运算不是很复杂时,数据流图的结构在代码中体现的还是很清楚的。但是一旦运算复杂,数据流图的结构就不是很清楚了。复杂的代码结构,重复的结点命名无疑都会给代码维护增加难度。为了让代码结构更加清楚,tensorflow 加入了 tf.name_scope 的设计。name_scope 可以 “向特定上下文中创建的所有指令添加名称范围前缀。” 看官方文档里的示例。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
c_0 = tf.constant(0, name="c") # => 指令名为 "c"
# 已被使用过的名字将会被 "唯一化".
c_1 = tf.constant(2, name="c") # => 指令名为 "c_1"
# name_scope 给在相同上下文里的所有指令添加了前缀
with tf.name_scope("outer"):
c_2 = tf.constant(2, name="c") # => 指令名为 "outer/c"
# name_scope 的嵌套就像分层文件系统的路径
with tf.name_scope("inner"):
c_3 = tf.constant(3, name="c") # => 指令名为 "outer/inner/c"
# 离开一个 name_scope 上下文时,指令名将会返回使用上一个前缀
c_4 = tf.constant(4, name="c") # => 指令名为 "outer/c_1"
# 已被使用过的 name_scope 名将会被 "唯一化".
with tf.name_scope("inner"):
c_5 = tf.constant(5, name="c") # => 指令名为 "outer/inner_1/c"
c_0 = tf.constant (0, name="c") # => 指令名为 "c" # 已被使用过的名字将会被 "唯一化". c_1 = tf.constant (2, name="c") # => 指令名为 "c_1" # name_scope 给在相同上下文里的所有指令添加了前缀 with tf.name_scope ("outer"): c_2 = tf.constant (2, name="c") # => 指令名为 "outer/c" # name_scope 的嵌套就像分层文件系统的路径 with tf.name_scope ("inner"): c_3 = tf.constant (3, name="c") # => 指令名为 "outer/inner/c" # 离开一个 name_scope 上下文时,指令名将会返回使用上一个前缀 c_4 = tf.constant (4, name="c") # => 指令名为 "outer/c_1" # 已被使用过的 name_scope 名将会被 "唯一化". with tf.name_scope ("inner"): c_5 = tf.constant (5, name="c") # => 指令名为 "outer/inner_1/c"
c_0 = tf.constant(0, name="c")  # => 指令名为 "c"

# 已被使用过的名字将会被"唯一化".
c_1 = tf.constant(2, name="c")  # => 指令名为 "c_1"

# name_scope给在相同上下文里的所有指令添加了前缀
with tf.name_scope("outer"):
  c_2 = tf.constant(2, name="c")  # => 指令名为 "outer/c"

  # name_scope的嵌套就像分层文件系统的路径
  with tf.name_scope("inner"):
    c_3 = tf.constant(3, name="c")  # => 指令名为 "outer/inner/c"

  # 离开一个name_scope上下文时,指令名将会返回使用上一个前缀
  c_4 = tf.constant(4, name="c")  # => 指令名为 "outer/c_1"

  # 已被使用过的name_scope名将会被"唯一化".
  with tf.name_scope("inner"):
    c_5 = tf.constant(5, name="c")  # => 指令名为 "outer/inner_1/c"

结合 python 的缩进,这样代码的结构就会清楚不少。而且在数据流图的可视化时,使用 name_scope 也可以适当的隐藏细节,以使整个计算结构更加清楚(如上文中的 GIF)。

不知各位有没有发现,讲了那么久的数据流图,然而我们的代码中似乎都没有出现一个明确的数据流图声明。我们的代码之所以能正确运作,其实是因为 tensorflow 会自行创建一张默认数据流图,而我们创建的指令都会被自动加入其中。我们可以通过 tf.get_default_graph 来获取默认图的引用。如果我们需要自己创建数据流图,可以调用其构造函数 tf.Graph。以下是新建一个图并在其中加入一个指令的代码。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
g = tf.Graph()
with g.as_default():
op_g_mul = tf.multiply(3,4)
g = tf.Graph() with g.as_default(): op_g_mul = tf.multiply(3,4)
g = tf.Graph()

with g.as_default():
    op_g_mul = tf.multiply(3,4)

会话

如果你尝试过打印之前的 Tensor,你会发现你并不能获得你想要的运算结果。事实上,tensorflow 的底层都是由 C++ 进行编写的,而 Python 部分只是承担 “指示操作” 的工作。而会话(Session)就相当于 Python(当然也有其他语言)和 C++ 运行时(C++ Runtime)之间的桥梁,所以数据图必须传入会话执行(有时,在编写基于其他 API 的代码时,例如使用 tf.estimator.Estimator 时我们并不需要显式的创建会话,事实上这些 API 本身已经实现了会话的创建和管理)。调用 tf.Session 我们便可以创建一个会话。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sess = tf.Session()
sess = tf.Session()
sess = tf.Session()

而调用 sess.run 函数,我们就可以计算一个 Tensor 对象的结果了。例如,我们希望计算前文图中的结果,可以如是编写。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
result = sess.run(op_mul)
print(result)
result = sess.run(op_mul) print(result)
result = sess.run(op_mul)
print(result)

输出为 8。不过当我们运行 sess.run (op_g_mul) 时却遇到了问题,程序抛出了 “ValueError: Tensor Tensor (“Mul:0”, shape=(), dtype=int32) is not an element of this graph”。其实,虽然我们没有直接传入,但是在创建会话的过程中,默认的数据流图已经被隐式传入了。而 op_g_mul 并不在默认图中,所以就抛出了错误。我们可以在创建会话时显式的指定图来解决这个问题。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sess = tf.Session(graph=g)
result = sess.run(op_g_mul)
print(result)
sess = tf.Session(graph=g) result = sess.run(op_g_mul) print(result)
sess = tf.Session(graph=g)
result = sess.run(op_g_mul)
print(result)

输出为 12。事实上,会话的构造函数还接受许多其它的参数。不过这些参数大多与我们当前的情形无关,所以暂且不进行介绍,有兴趣的朋友可以查阅官方文档 [2]。tf.Session.run 方法接受一个参数 fetches,此外还有三个可选参数:feed_dict、options、run_metadata。后面两个参数我们目前用不到,所以暂且不提。而前两个参数正好可以与两种特殊的张量结合,那我们就在下一节讲解。

fetches

fetches 接受一个或多个 tf.Tensor 或 tf.Operation,并会执行之。之所以能接受多个,是指这些对象可以以列表或是字典值的形式传入,而返回时也会保持这种形式。传入 tf.Tensor 的例子之前已经有了,不过 tf.Operation 是什么?之前不是说创建结点的函数会返回张量么,那 tf.Operation 从何而来?这里我们讲一个重要的函数 tf.global_variables_initializer,因为这个 tf.Variable 有关所以我们来讲讲变量。

变量

根据上一篇的教程我们知道,在学习时有一些量是会随着迭代而被更新的。而这些特殊的,会改变的张量在 tensorflow 中以 tf.Variable 的形式存在。创建变量的方法很简单,我们直接调用 tf.Variable 即可。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
v_int = tf.Variable(1, name="var_int")
v_mat = tf.Variable(tf.zeros((2,3)), name="var_mat")
v_int = tf.Variable(1, name="var_int") v_mat = tf.Variable(tf.zeros((2,3)), name="var_mat")
v_int = tf.Variable(1, name="var_int")
v_mat = tf.Variable(tf.zeros((2,3)), name="var_mat")

可以看到,张量对象也是可以被传入的。但是和之前相同,这些张量对象不会立刻被计算,所以我们就需要一些方法来初始化这些变量。tf.global_variables_initializer 函数就是初始化当前图中所有变量的函数,不过它也不会立刻被执行。事实上,tf.global_variables_initializer 会返回一个 tf.Operation 对象,换句话说,初始化变量也是一个指令。所以我们可以通过 sess.run (tf.global_variables_initializer ()) 来初始化所有变量。tensorflow 还提供了 tf.initialize_variables 来初始化指定变量,它接受一个变量列表。tf.Variable 还接受一个可选参数 trainable,如果他为否,那 tensorflow 自带的训练方法就不能改变它的值,只能由下文提到的方法改变。

有创建自然有修改。tf.Variable.assign 就是用于赋值的方法,它接受一个新值。它接受的基本和变量声明时接受的相同,不过值得注意的是,输入张量的形状要和声明时相同。这个方法最重要的还是其返回值,和其他指令一样 tf.Variable.assign 返回的是一个值为变量修改后值的张量。tf.Variable 还提供了 assign_add 和 assign_sub 方法以供自增自减,用法类似于 tf.Variable.assign。这里给出一段代码,大家可以通过猜测输出来测试下自己对变量的理解是否正确。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
v_int = tf.Variable(1, name="var_int")
v_int2 = v_int.assign(2)
v_int3 = v_int.assign_add(1)
sess.run(tf.global_variables_initializer())
print(sess.run(v_int))
print(sess.run(v_int2))
print(sess.run(v_int3))
print(sess.run(v_int3))
print(sess.run(v_int))
v_int = tf.Variable(1, name="var_int") v_int2 = v_int.assign(2) v_int3 = v_int.assign_add(1) sess.run(tf.global_variables_initializer()) print(sess.run(v_int)) print(sess.run(v_int2)) print(sess.run(v_int3)) print(sess.run(v_int3)) print(sess.run(v_int))
v_int = tf.Variable(1, name="var_int")

v_int2 = v_int.assign(2)
v_int3 = v_int.assign_add(1)

sess.run(tf.global_variables_initializer())
print(sess.run(v_int))
print(sess.run(v_int2))
print(sess.run(v_int3))
print(sess.run(v_int3))
print(sess.run(v_int))

#答案

feed_dict 与占位符

在训练过程中,我们需要将一些训练样本传入以进行计算。而当模型训练完毕之后,我们也需要传入一些数据以供模型进行预测。很明显,我们需要一个传入数据的方法,而占位符(placeholder)就是为此设计的。我们可以通过 tf.placeholder 来创建一个占位符。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
p_int = tf.placeholder(tf.int32, name="input_int")
p_int = tf.placeholder(tf.int32, name="input_int")
p_int = tf.placeholder(tf.int32, name="input_int")

tf.placeholder 接受一个参数 dtype 和两个可选参数 shape、name。dtype 即数据类型,shape 指定了占位符的形状,它默认为 None,即可接受任意形状的张量。name 指定了占位符在图中的名称。

可以看出,占位符的创建中并没有给占位符赋值。而给占位符以数据的方式,是在 tf.Session.run 的方法调用时传入 feed_dict。feed_dict 的键是一个张量对象,即创建占位符返回的张量对象,而值就是需要传入的张量。我们可以将上面的图进行一些修改。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
a = tf.placeholder(tf.int32, name="a")
b = tf.placeholder(tf.int32, name="b")
c = tf.placeholder(tf.int32, name="c")
op_add = tf.add(a, b, name="Add")
op_mul = tf.multiply(op_add, c, name="Multiply")
sess = tf.Session()
inputs = { a: 1, b: 3, c: 2 }
result = sess.run(op_mul, feed_dict=inputs)
print(result)
a = tf.placeholder(tf.int32, name="a") b = tf.placeholder(tf.int32, name="b") c = tf.placeholder(tf.int32, name="c") op_add = tf.add(a, b, name="Add") op_mul = tf.multiply(op_add, c, name="Multiply") sess = tf.Session() inputs = { a: 1, b: 3, c: 2 } result = sess.run(op_mul, feed_dict=inputs) print(result)
a = tf.placeholder(tf.int32, name="a")
b = tf.placeholder(tf.int32, name="b")
c = tf.placeholder(tf.int32, name="c")

op_add = tf.add(a, b, name="Add")
op_mul = tf.multiply(op_add, c, name="Multiply")

sess = tf.Session()
inputs = { a: 1, b: 3, c: 2 }
result = sess.run(op_mul, feed_dict=inputs)
print(result)

输出为 8。当然,我们也可以通过 tf.placeholder_with_default 函数创造一个带默认值的占位符,它接受两个参数 input、shape,一个可选参数 name。input 即默认值,其他与 tf.placeholder 相仿。比如,我们可以给上述图中的占位符 c 以默认值 2。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
c = tf.placeholder_with_default(2, shape=None, name="c")
c = tf.placeholder_with_default(2, shape=None, name="c")
c = tf.placeholder_with_default(2, shape=None, name="c")

那么,当我们的 feed_dict 中没有给 c 指定值时,它的值就会是默认值 2 了。

实践

经过上面的介绍,相信你对 tensorflow 已经有了一个基本的了解,那我们就以上篇教程中的感知机为例,简单介绍下在 tensorflow 中如何进行机器学习。

以感知机为例

还记得感知机吗?如果不记得,赶紧去看上一篇。我们先声明预测函数和代价函数。线性函数和判断函数是为了后续编码方便而声明的。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 线性函数
def linear(x, w, b):
return tf.matmul(x, tf.transpose(w)) + b
# 判断函数
def judge_func(x, w, b, y):
return linear(x, w, b) * y
# 预测函数
def predict(x, w, b):
return tf.sign(linear(x, w, b))
# 代价函数
def cost_func(x, w, b, y):
return -tf.reduce_sum(judge_func(x, w, b, y))
# 线性函数 def linear (x, w, b): return tf.matmul (x, tf.transpose (w)) + b # 判断函数 def judge_func (x, w, b, y): return linear (x, w, b) * y # 预测函数 def predict (x, w, b): return tf.sign (linear (x, w, b)) # 代价函数 def cost_func (x, w, b, y): return -tf.reduce_sum (judge_func (x, w, b, y))
# 线性函数
def linear(x, w, b):
    return tf.matmul(x, tf.transpose(w)) + b

# 判断函数
def judge_func(x, w, b, y):
    return linear(x, w, b) * y

# 预测函数
def predict(x, w, b):
    return tf.sign(linear(x, w, b))

# 代价函数
def cost_func(x, w, b, y):
    return -tf.reduce_sum(judge_func(x, w, b, y))

然后我们创建变量 w 和 b,并创建用于传入数据的占位符。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var_w = tf.Variable(tf.ones((1, n)), dtype=tf.float32, name="Weight")
var_b = tf.Variable(1, dtype=tf.float32, name="Bias")
p_X = tf.placeholder(tf.float32, shape=(None, n), name="input_X")
p_y = tf.placeholder(tf.float32, shape=(None, 1), name="input_y")
var_w = tf.Variable(tf.ones((1, n)), dtype=tf.float32, name="Weight") var_b = tf.Variable(1, dtype=tf.float32, name="Bias") p_X = tf.placeholder(tf.float32, shape=(None, n), name="input_X") p_y = tf.placeholder(tf.float32, shape=(None, 1), name="input_y")
var_w = tf.Variable(tf.ones((1, n)), dtype=tf.float32, name="Weight")
var_b = tf.Variable(1, dtype=tf.float32, name="Bias")
p_X = tf.placeholder(tf.float32, shape=(None, n), name="input_X")
p_y = tf.placeholder(tf.float32, shape=(None, 1), name="input_y")

然后就是训练了。首先声明 Session,初始化变量。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
init = tf.global_variables_initializer()
sess.run(init)
init = tf.global_variables_initializer() sess.run(init)
init = tf.global_variables_initializer()
sess.run(init)

下面来计算代价函数,并使用 tf.gradients 函数给代价求导(偏导),然后根据求得的梯度(grad_w 和 grad_b)更新两个参数。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cost = cost_func(p_X, var_w, var_b, p_y)
grad_w, grad_b = tf.gradients(cost, [var_w, var_b])
t_w = var_w.assign_sub(alpha * grad_w)
t_b = var_b.assign_sub(alpha * grad_b)
train_step = [t_w, t_b]
cost = cost_func(p_X, var_w, var_b, p_y) grad_w, grad_b = tf.gradients(cost, [var_w, var_b]) t_w = var_w.assign_sub(alpha * grad_w) t_b = var_b.assign_sub(alpha * grad_b) train_step = [t_w, t_b]
cost = cost_func(p_X, var_w, var_b, p_y)
grad_w, grad_b = tf.gradients(cost, [var_w, var_b])

t_w = var_w.assign_sub(alpha * grad_w)
t_b = var_b.assign_sub(alpha * grad_b)
train_step = [t_w, t_b]

到此为止,我们已经完成了计算图的搭建,那么接下来我们就来编写迭代更新的代码。首先我们需要获得当前所有的误分类点,这里运用了之前的判断函数并结合 python 的列表解析进行筛选。d_X 和 d_y 代表数据集。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
inputs = {p_X: d_X, p_y: d_y}
judge = sess.run(judge_func(p_X, var_w, var_b, p_y), feed_dict=inputs)
wrong_idx = [i for i in range(m) if judge[i][0]<0]
wrong_X = [d_X[i] for i in wrong_idx]
wrong_y = [d_y[i] for i in wrong_idx]
inputs = {p_X: d_X, p_y: d_y} judge = sess.run(judge_func(p_X, var_w, var_b, p_y), feed_dict=inputs) wrong_idx = [i for i in range(m) if judge[i][0]<0] wrong_X = [d_X[i] for i in wrong_idx] wrong_y = [d_y[i] for i in wrong_idx]
inputs = {p_X: d_X, p_y: d_y}
judge = sess.run(judge_func(p_X, var_w, var_b, p_y), feed_dict=inputs)
wrong_idx = [i for i in range(m) if judge[i][0]<0]
wrong_X = [d_X[i] for i in wrong_idx]
wrong_y = [d_y[i] for i in wrong_idx]

如果有误分类点,那我们就随机选择一个误分类点并更新参数。np 是 Numpy 库的别名,当然也可以使用 python 的 random 库,即将其改为 random.randint (0, len (wrong_idx) – 1)。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 随机取一个误分类点
idx = np.random.choice(len(wrong_idx))
X = [wrong_X[idx]]
y = [wrong_y[idx]]
# 更新参数
inputs = {p_X: X, p_y: y}
w, b = sess.run(train_step, feed_dict=inputs)
# 随机取一个误分类点 idx = np.random.choice (len (wrong_idx)) X = [wrong_X [idx]] y = [wrong_y [idx]] # 更新参数 inputs = {p_X: X, p_y: y} w, b = sess.run (train_step, feed_dict=inputs)
# 随机取一个误分类点
idx = np.random.choice(len(wrong_idx))
X = [wrong_X[idx]]
y = [wrong_y[idx]]
# 更新参数
inputs = {p_X: X, p_y: y}
w, b = sess.run(train_step, feed_dict=inputs)

这里需要注意,如果直接传入 wrong_X [idx] 的话是不行的。因为 wrong_X [idx] 是列表,被视为一阶的张量,但是占位符 p_X 却是二阶的。

然后计算当前的损失并打印。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 计算损失
inputs = {p_X: wrong_X, p_y: wrong_y}
loss = sess.run(cost, feed_dict=inputs)
# 打印
print("Step %d/%d, w = %s, b = %f, loss = %f." %
(i, steps, str(w), b, loss))
# 计算损失 inputs = {p_X: wrong_X, p_y: wrong_y} loss = sess.run (cost, feed_dict=inputs) # 打印 print ("Step % d/% d, w = % s, b = % f, loss = % f." % (i, steps, str (w), b, loss))
# 计算损失
inputs = {p_X: wrong_X, p_y: wrong_y}
loss = sess.run(cost, feed_dict=inputs)
# 打印
print("Step %d/%d, w = %s, b = %f, loss = %f." %
      (i, steps, str(w), b, loss))

这样我们便完成了一次迭代,之后重复进行筛选 - 更新的操作即可。运行之后打印如下。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Step 0/100, w = [[0.75 0.9 ]], b = 0.950000, loss = 11.350000.
Step 1/100, w = [[0.55 0.84999996]], b = 0.900000, loss = 9.299999.
Step 2/100, w = [[0.3 0.74999994]], b = 0.850000, loss = 6.650000.
Step 3/100, w = [[0.05000001 0.6499999 ]], b = 0.800000, loss = 4.000000.
Step 4/100, w = [[-0.19999999 0.5499999 ]], b = 0.750000, loss = 1.350000.
Step 5/100, w = [[-0.39999998 0.49999988]], b = 0.700000, loss = -0.700000.
Step 0/100, w = [[0.75 0.9 ]], b = 0.950000, loss = 11.350000. Step 1/100, w = [[0.55 0.84999996]], b = 0.900000, loss = 9.299999. Step 2/100, w = [[0.3 0.74999994]], b = 0.850000, loss = 6.650000. Step 3/100, w = [[0.05000001 0.6499999 ]], b = 0.800000, loss = 4.000000. Step 4/100, w = [[-0.19999999 0.5499999 ]], b = 0.750000, loss = 1.350000. Step 5/100, w = [[-0.39999998 0.49999988]], b = 0.700000, loss = -0.700000.
Step 0/100, w = [[0.75 0.9 ]], b = 0.950000, loss = 11.350000.
Step 1/100, w = [[0.55       0.84999996]], b = 0.900000, loss = 9.299999.
Step 2/100, w = [[0.3        0.74999994]], b = 0.850000, loss = 6.650000.
Step 3/100, w = [[0.05000001 0.6499999 ]], b = 0.800000, loss = 4.000000.
Step 4/100, w = [[-0.19999999  0.5499999 ]], b = 0.750000, loss = 1.350000.
Step 5/100, w = [[-0.39999998  0.49999988]], b = 0.700000, loss = -0.700000.

从图中我们也可以看到,这个超平面(此处是直线)确实将数据集分为了两块。

训练结果

使用 Optimizer 训练模型

除了自行使用梯度进行训练,tensorflow 其实本身就提供了很多算法以供模型训练。在 tensorflow 里这些算法都以 Optimizer 的形式存在,而这里我们使用 tf.train.GradientDescentOptimizer 来进行代价函数的最小化。tf.train.GradientDescentOptimizer 接受一个参数 learning_rate 即学习率,也就是 alpha。除此之外,它还接受两个可选参数 use_locking 和 name,后者和其他 name 一样,前者暂时不进行介绍。那么创建了一个 Optimizer 之后,我们只要调用其 minimize 方法(返回一个 tf.Operation 对象)并传入代价函数的张量就可以顺利的进行训练了。对上面的代码进行微小的调整即可,构建图可以简化为。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cost = cost_func(p_X, var_w, var_b, p_y)
train_step = tf.train.GradientDescentOptimizer(alpha).minimize(cost)
cost = cost_func(p_X, var_w, var_b, p_y) train_step = tf.train.GradientDescentOptimizer(alpha).minimize(cost)
cost = cost_func(p_X, var_w, var_b, p_y)
train_step = tf.train.GradientDescentOptimizer(alpha).minimize(cost)

同时,参数更新需要更改为。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 更新参数
inputs = {p_X: wrong_X, p_y: wrong_y}
sess.run(train_step, feed_dict=inputs)
w, b = sess.run([var_w, var_b])
# 更新参数 inputs = {p_X: wrong_X, p_y: wrong_y} sess.run (train_step, feed_dict=inputs) w, b = sess.run ([var_w, var_b])
# 更新参数
inputs = {p_X: wrong_X, p_y: wrong_y}
sess.run(train_step, feed_dict=inputs)
w, b = sess.run([var_w, var_b])

运行之后打印如下。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Step 0/50, w = [[0.55 0.85]], b = 0.900000, loss = 9.300000.
Step 1/50, w = [[0.10000001 0.70000005]], b = 0.800000, loss = 4.600000.
Step 2/50, w = [[-0.35 0.5500001]], b = 0.700000, loss = -0.100000.
Step 3/50, w = [[-0.6 0.45000008]], b = 0.650000, loss = -1.450000.
Step 0/50, w = [[0.55 0.85]], b = 0.900000, loss = 9.300000. Step 1/50, w = [[0.10000001 0.70000005]], b = 0.800000, loss = 4.600000. Step 2/50, w = [[-0.35 0.5500001]], b = 0.700000, loss = -0.100000. Step 3/50, w = [[-0.6 0.45000008]], b = 0.650000, loss = -1.450000.
Step 0/50, w = [[0.55 0.85]], b = 0.900000, loss = 9.300000.
Step 1/50, w = [[0.10000001 0.70000005]], b = 0.800000, loss = 4.600000.
Step 2/50, w = [[-0.35       0.5500001]], b = 0.700000, loss = -0.100000.
Step 3/50, w = [[-0.6         0.45000008]], b = 0.650000, loss = -1.450000.

从训练结果图中我们可以看出,经过训练的模型可以正确分类。

训练结果(使用 Optimizer)

可视化:Tensorboard

Tensorflow 还自带了一个很棒的可视化工具 ——Tensorboard,那么我们就来看看如何使用这个工具。首先我们要修改我们的代码,以记录一些能供 Tensorboard 显示的数据。这里我们需要使用 tf.summary.FileWriter,我们来创建一个 FileWriter。我们需要传入一个目录以存储相应数据,如果需要将图可视化也可以传入相应的图。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
writer = tf.summary.FileWriter('./perceptron_logs', sess.graph)
writer = tf.summary.FileWriter('./perceptron_logs', sess.graph)
writer = tf.summary.FileWriter('./perceptron_logs', sess.graph)

能在 Tensorboard 中显示的数据的组织形式是 summary,它可以记录各种类型的数据。暂且只讲解标量(scalar)和图像(image)的记录。可以通过调用 tf.summary.scalar 来记录一个标量,它接受标量的名称和一个张量。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
tf.summary.scalar('loss', cost)
tf.summary.scalar('loss', cost)
tf.summary.scalar('loss', cost)

这个函数会返回一个 tf.Operation,而只有我们手动在会话中执行这个指令,标量才会被记录。随着需要记录的数据增多,手动逐个调用是非常繁琐的,所以 tensorflow 就提供了一个方法 tf.summary.merge_all 来把所有 summry 指令合并为一个指令,所以我们暂且不需要记录它的返回值。

记录图的操作会稍微复杂一点。因为记录图像的初衷是为了调试能生成图像的一些模型,所以记录的图像是以张量的形式存储的。而要记录 Matplot 库绘制的图像,我们首先要将其转换为张量形式。这个函数可以将当前绘制的图像转为张量并返回。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def gen_plot():
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
image = tf.image.decode_png(buf.getvalue(), channels=4)
image = tf.expand_dims(image, 0)
return image
def gen_plot(): buf = io.BytesIO() plt.savefig(buf, format='png') buf.seek(0) image = tf.image.decode_png(buf.getvalue(), channels=4) image = tf.expand_dims(image, 0) return image
def gen_plot():
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    image = tf.image.decode_png(buf.getvalue(), channels=4)
    image = tf.expand_dims(image, 0)
    return image

我们声明一个新函数以绘制图像,最后调用上述函数返回一个张量。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def draw_image():
"""绘制图像"""
plt.close('all')
# 绘制过程略
return gen_plot()
def draw_image (): """绘制图像""" plt.close ('all') # 绘制过程略 return gen_plot ()
def draw_image():
    """绘制图像"""
    plt.close('all')

    # 绘制过程略

    return gen_plot()

这样,我们就可以通过调用 draw_image 函数来生成一张图片,下面我们来记录它。如果要直接记录这张图片,可以直接在最后执行如下代码。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
op_summary = tf.summary.image("Train result", draw_image())
summary = sess.run(op_summary)
writer.add_summary(summary)
op_summary = tf.summary.image("Train result", draw_image()) summary = sess.run(op_summary) writer.add_summary(summary)
op_summary = tf.summary.image("Train result", draw_image())
summary = sess.run(op_summary)
writer.add_summary(summary)

不过如果我们希望记录训练过程中的图像,就不能这么写了。因为这样记录的图像并不会依照迭代次数归类,而是会依单张的形式存在,一旦图像较多,那么 Tensorboard 内就会非常混乱。所以我们需要用一个变量来存储这张图。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var_img = tf.Variable(tf.ones((1,480,640,4), dtype=tf.uint8), dtype=tf.uint8, name="Plot")
var_img = tf.Variable(tf.ones((1,480,640,4), dtype=tf.uint8), dtype=tf.uint8, name="Plot")
var_img = tf.Variable(tf.ones((1,480,640,4), dtype=tf.uint8), dtype=tf.uint8, name="Plot")

这里的张量形状和图片大小有关,暂不深入讲解。下面,我们需要记录这张图。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
tf.summary.image("Train result", var_img)
tf.summary.image("Train result", var_img)
tf.summary.image("Train result", var_img)

每次迭代我们都需要生成并更新变量以记录新图片。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sess.run(var_img.assign(draw_image()))
sess.run(var_img.assign(draw_image()))
sess.run(var_img.assign(draw_image()))

这样我们就能记录一系列图片了。

记录完数据之后,我们在对应文件夹下打开控制台,并键入 tensorboard –logdir=perceptron_logs。等待片刻后复制地址就可以在浏览器中打开了。

打开网页之后,就可以点击上方的标签页来查看相关数据了。

左侧的工具栏也可以对页面内容进行调整。可以看到,使用 Tensorboard 可以大大降低数据可视化的难度。

SL 大法:保存检查点

当我们成功训练了一个模型之后,我们可能会希望保存下这个模型中的变量,以供之后预测。除此之外,在训练一个复杂的模型的过程中,定时保存当前的训练结果也是很重要的,这样一旦发生意外,也可以从就近的检查点(checkpoint)进行恢复。在 tensorflow 中,我们需要通过 tf.train.Saver 来进行储存。tf.train.Saver 的构造方法并不要求传入任何参数,但是接受许多可选参数,感兴趣的朋友可以查看相关文档 [6]。创建 saver 对象之后,我们可以调用 tf.train.Saver.save 方法来储存一个检查点。它接受 sess 和 save_path 两个参数,前者接受一个 tf.Session 对象,后者接受一个路径以存储文件。它同样具有很多可选参数,不过其中 global_step 比较实用,如果传入,他就会在文件名后加上当前的步数。比如 “filepath”=>“filepath-1”。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
saver = tf.train.Saver()
saver.save(sess, './percetron')
saver = tf.train.Saver() saver.save(sess, './percetron')
saver = tf.train.Saver()
saver.save(sess, './percetron')

复原时,只需要调用 tf.train.Saver.restore。他只接受 sess 和 save_path 两个参数,并且没有可选参数,用法与 tf.train.Saver.save 相同。不过需要注意的是,tf.train.Saver.restore 并不能指定 global_step,所以要恢复相应的检查点,只能通过手动的加上 “- 步数”。

代码汇总

这里是上述代码的汇总。通过更改 train 为 train_with_optimizer,你可以使用 tensorflow 的 Optimizer 进行训练。train_with_optimizer 没有加入 summary 的存储,你可以自行加入。Gist 点击此处

#代码汇总

后记

感谢你阅读这篇博客。我写这篇文章的初衷是想以一篇简明扼要的文章快速介绍 Tensorflow 这个机器学习框架的,但是没想到,随着各种知识的扩充,这篇文章最后都快接近两万字了。其实现在流行的机器学习框架很多,光是支持 Python 的同类框架就还有 MXNet、Theano、PyTorch 等等。其中尤其是 PyTorch,编写出来的代码短小精悍,而且各方面完全没有比 tesorflow 差。但是我最后还是选择了 tensorflow,原因就是 tensorflow 更广。诚然,可视化 PyTorch 有 Visdom,各类缺失的函数也有第三方库予以补足,但是 tensorflow 基本上都以一套很完整的体系囊括进来了。甚至把训练好的模型迁移到手机端 APP 也非常容易,这样的广度,目前所有能做到的且支持 Python 的框架,我想很难找到第二个了。当然,调试时 tensorflow 可能会显得速度较慢,这里推荐一个库 miniflow。它在基本兼容 tensorflow API 的基础上大大提升了运行速度,可以说很适合调试。

本文在匆忙中写就,而且由于作者水平问题,文章难免有很多疏漏。希望各位大神能及时指出,直接评论即可。如有疑问,可以发邮件至 admin@kaaass.net,能力范围内的问题我会择日回答。再次感谢你能耐心的读完这么长的博文。

Reference

  1. 在 Windows 上安装 TensorFlow (https://www.tensorflow.org/install/install_windows)
  2. 图和会话 (https://www.tensorflow.org/programmers_guide/graphs)
  3. 张量的阶、形状、数据类型 (http://wiki.jikexueyuan.com/project/tensorflow-zh/resources/dims_types.html)
  4. Tensor Ranks, Shapes, and Types (https://www.tensorflow.org/versions/r1.1/programmers_guide/dims_types)
  5. Tensorflow: How to Display Custom Images in Tensorboard (e.g. Matplotlib Plots) (https://stackoverflow.com/questions/38543850/tensorflow-how-to-display-custom-images-in-tensorboard-e-g-matplotlib-plots)
  6. tf.train.Saver (https://www.tensorflow.org/api_docs/python/tf/train/Saver)
分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片
  4. 没有图片

评论

  1. 开发者头条 2018.07.13 9:57 上午

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/abli33 欢迎点赞支持!使用开发者头条 App 搜索 69380 即可订阅《KAAAsS Blog》

在此评论中不能使用 HTML 标签。