├── README.md
├── ch01
├── ch1-tensorflow-summary.md
├── exper
│ ├── images
│ │ ├── cuda.jpg
│ │ ├── cuda2.jpg
│ │ ├── cuda3.png
│ │ ├── cudnn.jpg
│ │ └── cudnn2.jpg
│ └── tensorflow-cpu gpu安装教程.md
└── images
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ └── tree-def.png
├── ch02
└── ch2-graph-and-sesseion.md
├── ch03
└── ch3-edge-and-node.md
├── ch04
└── ch4-constant-variable-placeholder.md
├── ch05
└── ch5-name-and-scope.md
├── ch06
└── ch6-IO.md
├── ch07
└── ch7-queue-and-thread.md
├── ch08
├── ch8-dataset-01.md
└── ch8-dataset-02.md
├── ch09
└── ch9-saver.md
├── ch10
└── ch10-eager.md
├── ch11
├── ch11-tensorboard.md
└── images
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── audio.png
│ ├── connected_series.png
│ ├── contral_dependency.png
│ ├── dataflow.png
│ ├── distributions.png
│ ├── embeddings.png
│ ├── graph_1.png
│ ├── graph_2.png
│ ├── graph_icon.png
│ ├── histograms.png
│ ├── image.png
│ ├── scalars.png
│ ├── text.png
│ └── unconnected_series.png
├── exper
├── 01.graph and session.py
├── 02.control_dependencies.py
├── 02.tensor slice.py
└── 03.knn.py
└── images
├── AnimatedFileQueues.gif
├── graph.png
├── graph_add.png
├── graph_more.png
├── sub_graph.png
└── thchs30-train-wav-v2.jpg
/README.md:
--------------------------------------------------------------------------------
1 | # TensorFlow基础教程
2 |
3 | 基于TensorFlow 1.8版本的中文基础教程。适用于有Python基础者入门。
4 |
5 | **目录:**
6 |
7 | * 第一章 TensorFlow概述
8 | * 第二章 图与会话
9 | * 第三章 图的边与节点
10 | * 第四章 常量、变量、占位符
11 | * 第五章 名字与作用域
12 | * 第六章 文件IO与模型存取
13 | * 第七章 队列与线程
14 | * 第八章 数据集文件操作
15 | * 第九章 模型存取
16 | * 第十章 Eager模式
17 | * 第十一章 TensorBoard基础用法
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ch01/ch1-tensorflow-summary.md:
--------------------------------------------------------------------------------
1 | 本节主要内容:
2 |
3 | - 理解Tensorflow是什么。
4 | - 理解Tensorflow所使用的编程模式。
5 | - 了解Tensorflow高层库,以及高层库与其关系。
6 | - Tensorflow的作用。
7 |
8 | # Tensorflow概述
9 |
10 | Tensorflow是由Google Brain Team开发的使用**数据流图**进行数值计算的开源机器学习库。Tensorflow的一大亮点是支持异构设备分布式计算(heterogeneous distributed computing)。这里的异构设备是指使用CPU、GPU等计算设备进行有效地协同合作。
11 |
12 | *Google Brain Team与DeepMind是独立运行相互合作的关系。*
13 |
14 | Tensorflow拥有众多的用户,除了Alphabet内部使用外,ARM、Uber、Twitter、京东、小米等众多企业均使用Tensorflow作为机器学习的工具。
15 |
16 | 
17 |
18 |
19 |
20 | 常见机器学习库包括Tensorflow、MXNet、Torch、Theano、Caffe、CNTK、scikit-learn等。
21 |
22 | | 库 | 维护人员或机构 | 支持语言 | 支持操作系统 |
23 | | :------------------------------------------: | :------------------------------------ | :--------------------------------: | :----------------------------------: |
24 | | [Tensorflow](https://www.tensorflow.org/) | google | Python、C++、Go | Linux、mac os、Android、iOS、Windows |
25 | | [MXNet](https://mxnet.incubator.apache.org/) | 分布式机器学习社区(DMLC) | Python、Scala、R、Julia、C++、Perl | Linux、mac os、Android、iOS、Windows |
26 | | [Torch/PyTorch](http://torch.ch/) | Ronan Collobert等人 | Lua、LuaJIT、C/Python | Linux、mac os、Android、iOS、Windows |
27 | | Theano | 蒙特利尔大学( Université de Montréal) | Python | Linux、mac os、Winodws |
28 | | Computational Network Toolkit(CNTK) | 微软研究院 | Python、C++、BrainScript | Linux、Windows |
29 | | Caffe | 加州大学伯克利分校视觉与学习中心 | Python、C++、MATLAB | Linux、mac os、Windows |
30 | | PaddlePaddle | 百度 | Python、C++ | Linux、mac os |
31 |
32 |
33 |
34 | 各个框架对比https://github.com/zer0n/deepframeworks
35 |
36 | 2016-2017年框架热度对比(来源于谷歌趋势):
37 |
38 | 
39 |
40 |
41 |
42 | 2017-2018年框架热度对比(来源于谷歌趋势):
43 |
44 | 
45 |
46 | ## 什么是Tensorflow
47 |
48 | ### Tensor
49 |
50 | Tensor是张量的意思,原本在物理学中用来描述大于等于2维的量进行量纲分析的工具。我们早已熟知如何处理0维的量(纯量)、1维的量(向量)、2维的量(矩阵)。对于高维的数据,我们也需要一个工具来表述,这个工具正是张量。
51 |
52 | 张量类似于编程语言中的多维数组(或列表)。广义的张量包括了常量、向量、矩阵以及高维数据。在处理机器学习问题时,经常会遇到大规模样本与大规模计算的情况,这时候往往需要用到张量来进行计算。Tensorflow中张量是最重要与基础的概念。
53 |
54 | ### Flow
55 |
56 | flow是“流”的意思,这里可以可以理解为数据的流动。Tensorflow所表达的意思就是“张量流”。
57 |
58 | ### 编程模式
59 |
60 | 编程模式通常分为**命令式编程(imperative style programs)**和**符号式编程(symbolic style programs)**。命令式编程,直接执行逻辑语句完成相应任务,容易理解和调试;符号式编程涉及较多的嵌入和优化,很多任务中的逻辑需要使用图进行表示,并在其他语言环境中执行完成,不容易理解和调试,但运行速度有同比提升。机器学习中,大部分现代的框架使用符号式编程,其原因是编写程序容易且运行速度快。
61 |
62 | 命令式编程较为常见,例如直接使用C++、Python进行编程。例如下面的代码:
63 |
64 | ```python
65 | import numpy as np
66 | a = np.ones([10,])
67 | b = np.ones([10,]) * 5
68 | c = a + b
69 | ```
70 |
71 | 当程序执行到最后一句时,a、b、c三个变量有了值。程序执行的是真正的计算。
72 |
73 | 符号式编程不太一样,仍然是完成上述功能,使用符号式编程的写法如下(伪代码):
74 |
75 | ```python
76 | a = Ones_Variables('A', shape=[10,])
77 | b = Ones_Variables('B', shape=[10,])
78 | c = Add(a, b)
79 |
80 | # 计算
81 | Run(c)
82 | ```
83 |
84 | 上述代码执行到c=Add(a, b)时,并不会真正的执行加法运算,同样的a、b也并没有对应的数值,a、b、c均是一个符号,符号定义了执行运算的结构,我们称之为**计算图**,计算图没有执行真正的运算。当执行Run(c)时,计算图开始真正的执行计算,计算的环境通常不是当前的语音环境,而是C++等效率更高的语言环境。
85 |
86 | 机器学习库中,Tensorflow、theano使用了符号式编程(tensorflow借鉴了theano的很多优点);Torch使用了命令式编程;caffe、mxnet采用了两种编程模式混合的方式。
87 |
88 | ### 数据流图
89 |
90 | 当我们使用计算图来表示计算过程时,事实上可以看做是一个**推断**过程。在推断时,我们输入一些数据,并使用符号来表示各种计算过程,最终得到一个或多个推断结果。所以使用计算图可以在一定程度上对计算结果进行预测。
91 |
92 | 计算图在推断的过程中也是数据流转的过程,所以我们也可以称之为**数据流图**。举个例子,假如我们计算$(a+b)*(b+1)$的值,那么我们画出其数据流图,如下:
93 |
94 | 
95 |
96 | 输入a与b,通过不同通路进行计算并传入下一个节点。这一过程就是数据流动的过程。有了数据流图,我们还可以进行更多的操作,例如自动求微分等,在此不做赘述。
97 |
98 | ### Tensorflow高层库
99 |
100 | Tensorflow本质上是数值计算库,在数值处理与计算方面比较方便、灵活。虽然Tensorflow为机器学习尤其是深度学习提供了很多便捷的API,但在构建算法模型时,仍然较为复杂。为此Tensorflow官方以及众多第三方机构与个人开发了很多的使用简便的高层库,这些库与Tensorflow完全兼容,但可以极大简化模型构建、训练、部署等操作。其中较为常用工具包与高层库为:
101 |
102 | 1. TF Learn(tf.contrib.learn):类似于scikit-learn的使用极少代码量即可构建机器学习算法的工具包。
103 |
104 | TF Learn可以尽量使用最少的代码构建我们想要的模型,如下,我们构建一个神经网络模型只需要一行代码:
105 |
106 | ```python
107 | # 使用 TF Learn 定义一个神经网络
108 | tf.contrib.learn.DNNClassifier(feature_columns=feature_columns,
109 | hidden_units=[10, 200, 10],
110 | n_classes=3,
111 | model_dir="./my_model")
112 | ```
113 |
114 | 2. TF Slim(tf.contrib.slim):一个轻量级的用于定义、训练、评估深度学习算法的Tensorflow工具包。
115 |
116 | TF Slim只能用于深度学习算法,相比于 TF Learn 。TF Slim既可以方便的设计算法,又可以较大程度的定制模型。例如我们定制一个神经网络的层:
117 |
118 | ```python
119 | padding = 'SAME'
120 | initializer = tf.truncated_normal_initializer(stddev=0.01)
121 | regularizer = slim.l2_regularizer(0.0005)
122 | net = slim.conv2d(inputs, 64, [11, 11], 4,
123 | padding=padding,
124 | weights_initializer=initializer,
125 | weights_regularizer=regularizer,
126 | scope='conv1')
127 | ```
128 |
129 | 可以看到相比 TF Learn,要复杂的多,但与Tensorflow相比仍然要简单的多。
130 |
131 | 3. 高级API:Keras,TFLearn,Pretty Tensor
132 |
133 | 高级API是建立在Tensorflow API之上的API,其尽量抽象了Tensorflow的底层设计,并拥有一套自己完整、简洁、高效的API。可以以极简的语法设计算法模型。最重要的是高级API相比其他API,其功能更丰富,可以独立的不借助Tensorflow的API运行,同时也可以兼容Tensorflow的API。
134 |
135 | 在Tensorflow所有的高级API中,Keras是发展最好的一个。Keras主要用在设计神经网络模型之上,可用于快速将想法变为结果。Keras由纯Python编写而成并基Tensorflow、Theano以及CNTK后端。
136 |
137 | Keras的设计原则是(来自于官网):
138 |
139 | - 用户友好:Keras是为人类而不是天顶星人设计的API。用户的使用体验始终是我们考虑的首要和中心内容。Keras遵循减少认知困难的最佳实践:Keras提供一致而简洁的API, 能够极大减少一般应用下用户的工作量,同时,Keras提供清晰和具有实践意义的bug反馈。
140 | - 模块性:模型可理解为一个层的序列或数据的运算图,完全可配置的模块可以用最少的代价自由组合在一起。具体而言,网络层、损失函数、优化器、初始化策略、激活函数、正则化方法都是独立的模块,你可以使用它们来构建自己的模型。
141 | - 易扩展性:添加新模块超级容易,只需要仿照现有的模块编写新的类或函数即可。创建新模块的便利性使得Keras更适合于先进的研究工作。
142 | - 与Python协作:Keras没有单独的模型配置文件类型(作为对比,caffe有),模型由python代码描述,使其更紧凑和更易debug,并提供了扩展的便利性。
143 |
144 |
145 |
146 | 我们可以把 TF Learn 看做是开箱即用的工具,我们几乎不需要了解工具有关的理论,也不需要对工具进行任何修改就能使用;TF Slim 可以看做小巧的多功能改锥,可以快速的更换零件,但功能仅限于改锥,不能做别的;Keras就像是瑞士军刀,有了它既可以快速完成很多任务,又拥有丰富的拓展性。
147 |
148 | ### Tensorflow的发展
149 |
150 | 2015年11月9日,Tensorflow的0.5的版本发布并开源。起初Tensorflow的运行效率低下,不支持分布式、异构设备;
151 |
152 | 2016年4月,经过不到半年的时间发布了0.8版本,开始支持分布式、多GPU运算等,同期大多数机器学习框架对这些重要功能仍然不支持或支持有限;
153 |
154 | 2016年6月,0.9的版本改进了对移动设备的支持,到此,Tensorflow已经成为了为数不多的支持分布式、异构设备的开源机器学习库,并极大的改善了运算效率问题,成为运算效率最高的机器学习算法库之一。
155 |
156 | 2017年2月,Tensorflow的1.0正式版发布,增加了专用的编译器XLA、调试工具Debugger和`tf.transform`用来做数据预处理,并开创性的设计了`Tensorflow Fold`用于弥补符号编程在数据预处理时的缺陷。
157 |
158 | 2018年3月,TensorFlow1.7发布,将Eager模式加入核心API,从此TensorFlow具备了使用命令式编程模式进行编程的方法,极大的提高了TensorFlow的易用性。在2017年到2018年这一年中,PyTorch诞生并快速发展,PyTorch使用更加灵活的命令式编程模式给予了TensorFlow重要启示。
159 |
160 | 2018年7月,TensorFlow1.9发布,提出了`AutoGraph`模式,将符号式编程与命令式编程的优点相结合。这也标志着TensorFlow从探索阶段进入到了成熟阶段,在编程模式之争中逐步确立了独特的、便捷的、高效的新方法。
161 |
162 | TensorFlow在不到三年的发展中,因其快速迭代,适应发展要求而被广大用户所喜爱,目前成为了领先的机器学习框架,然而TensorFlow也有众多缺点为人所诟病,其中较高的学习成本、较快的API变动、复杂重复的高级API等为突出问题,好在TensorFlow社区称在2018年底会发布2.0版本,届时会剥离高层API、简化TensorFlow,并提高API的稳定性。
163 |
164 | ## Tensorflow能干什么?
165 |
166 | 设计、训练、部署机器算法。例如可以轻松设计很多有意思的算法,例如:
167 |
168 | **图像风格转换**: [**neural-style**](https://github.com/anishathalye/neural-style)
169 |
170 | 
171 |
172 | **游戏内的自动驾驶**:[**TensorKart**](https://github.com/kevinhughes27/TensorKart):
173 |
174 | 
175 |
176 | **目标检测**:[**SSD-Tensorflow**](https://github.com/balancap/SSD-Tensorflow)
177 |
178 | 
179 |
180 | TensorFlow支持部署算法到多种设备之上。
181 |
182 | 
183 |
184 |
--------------------------------------------------------------------------------
/ch01/exper/images/cuda.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/exper/images/cuda.jpg
--------------------------------------------------------------------------------
/ch01/exper/images/cuda2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/exper/images/cuda2.jpg
--------------------------------------------------------------------------------
/ch01/exper/images/cuda3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/exper/images/cuda3.png
--------------------------------------------------------------------------------
/ch01/exper/images/cudnn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/exper/images/cudnn.jpg
--------------------------------------------------------------------------------
/ch01/exper/images/cudnn2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/exper/images/cudnn2.jpg
--------------------------------------------------------------------------------
/ch01/exper/tensorflow-cpu gpu安装教程.md:
--------------------------------------------------------------------------------
1 | Tensorflow的安装分为CPU与GPU两种方式。后者安装需要确保机器安装有**英伟达显卡**并且英伟达Cuda支持此显卡。如果安装GPU版本的Tensorflow,那么首先需要安装Cuda、CuDNN等软件。
2 |
3 | 本教程使用的操作系统是Ubuntu16.04。推荐大家使用这个操作系统。
4 |
5 |
6 |
7 | ## 基础软件安装
8 |
9 | 如果您的系统中未安装Python以及相关依赖,首先需要安装这些软件。这里我们推荐安装Python3.6。
10 |
11 | ### 1. 安装Python
12 |
13 | 方法一:使用apt安装:
14 |
15 | ~~~shell
16 | sudo apt-get update # 升级apt本地索引
17 | sudo apt-get install python3 # 安装最新的python3
18 | ~~~
19 |
20 | 方法二:使用brew安装:
21 |
22 | ~~~shell
23 | brew install python3
24 | ~~~
25 |
26 | ###2. 安装Python虚拟环境
27 |
28 | ~~~shell
29 | sudo apt-get install python3-pip python3-dev python-virtualenv
30 | ~~~
31 |
32 | 创建与进入虚拟环境:
33 |
34 | ~~~shell
35 | # 创建targetDirectory虚拟环境
36 | virtualenv --system-site-packages -p python3 targetDirectory
37 | # 进入虚拟环境
38 | source ~/tensorflow/bin/activate
39 | ~~~
40 |
41 | 进入虚拟环境之后需要确保安装的pip大于等于8.1的版本。执行:
42 |
43 | ~~~shell
44 | easy_install -U pip
45 | ~~~
46 |
47 |
48 |
49 | ## CPU版Tensorflow安装教程
50 |
51 | CPU版Tensorflow安装方法有很多种,这里我们介绍两种。
52 |
53 | ### 方法一:使用pip安装
54 |
55 | 在虚拟环境中执行以下命令:
56 |
57 | ~~~shell
58 | pip install tensorflow
59 | ~~~
60 |
61 | 等待安装完毕即可。
62 |
63 | 可以测试一下Tensorflow是否成功安装。在安装了Tensorflow的虚拟环境中进入Python交互式环境,运行:
64 |
65 | ```python
66 | import tensorflow as tf
67 | hello = tf.constant('Hello, TensorFlow!')
68 | sess = tf.Session()
69 | print(sess.run(hello))
70 | ```
71 |
72 | 输出`b'hello TensorFlow'`。则表示成功安装。
73 |
74 |
75 |
76 | 这种方法安装最为简便,但可能无法发挥CPU的所有性能,如果需要让算法跑的更快一些,可以进行编译安装。
77 |
78 | ###方法二:编译安装
79 |
80 | 这里,我们推荐使用Bazel进行编译安装。
81 |
82 | 1. 确认安装了gcc,并且推荐使用gcc4。
83 |
84 | 2. 安装bazel
85 |
86 | 可以使用apt安装:
87 |
88 | ```shell
89 | sudo apt-get update && sudo apt-get install bazel
90 | ```
91 |
92 | 如果安装出错或者提示缺少依赖,可以参考[此处](https://docs.bazel.build/versions/master/install-ubuntu.html)。
93 |
94 | 3. 安装Python依赖:
95 |
96 | ~~~python
97 | sudo apt-get install python3-numpy python3-dev python3-pip python3-wheel
98 | ~~~
99 |
100 | 也可以在虚拟环境中使用pip进行安装。
101 |
102 | 4. 下载Tensorflow。
103 |
104 | ~~~shell
105 | git clone https://github.com/tensorflow/tensorflow
106 | ~~~
107 |
108 | 进入Tensorflow,并切换到最新版(目前为v1.3)。
109 |
110 | ~~~shell
111 | cd tensorflow
112 | git checkout v1.3.0
113 | ~~~
114 |
115 | 5. 执行configure
116 |
117 | ~~~shell
118 | ./configure
119 | ~~~
120 |
121 | 6. 使用bazel构建pip包:
122 |
123 | ~~~shell
124 | bazel build -c opt --copt=-march=native //tensorflow/tools/pip_package:build_pip_package
125 | ~~~
126 |
127 | 7. 生成whl文件。生成的文件放在/tmp/tensorflow_pkg目录中。执行:
128 |
129 | ~~~shell
130 | bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
131 | ~~~
132 |
133 | 8. 使用pip安装生成的whl文件(注意,我这里是`tensorflow-1.3.0-cp35-cp35m-macosx_10_11_x86_64.whl`,不同的操作系统与Python版本,生成的whl文件名也不同)。:
134 |
135 | ~~~shell
136 | pip install /tmp/tensorflow_pkg/tensorflow-1.3.0-py3-none-any.whl
137 | ~~~
138 |
139 | 到此安装完毕。可以测试一下Tensorflow是否成功安装。在安装了Tensorflow的虚拟环境中进入Python交互式环境,运行:
140 |
141 | ~~~python
142 | import tensorflow as tf
143 | hello = tf.constant('Hello, TensorFlow!')
144 | sess = tf.Session()
145 | print(sess.run(hello))
146 | ~~~
147 |
148 | 输出`b'hello TensorFlow'`。则表示成功安装。
149 |
150 |
151 |
152 | ##GPU版Tensorflow安装教程
153 |
154 | GPU版本的安装首先需要安装英伟达的CUDA与CuDNN。然后才需要安装Tensorflow,Tensorflow的安装也可以使用pip或者源码编译等方法安装。这里着重介绍CUDA的安装。安装流程如下:
155 |
156 | ### 1. 验证安装
157 |
158 | * 确保电脑安装了英伟达的显卡并且支持cuda。[在此查看](https://developer.nvidia.com/cuda-gpus)
159 |
160 | * 确认安装了gcc,并且推荐使用gcc4。
161 |
162 | 有些操作系统
163 |
164 | 注意:有些操作系统安装了某些开源驱动需要禁用才行。
165 |
166 | ### 2. 下载并安装cuda
167 |
168 | 选择对应的操作系统版本,下载cuda文件。[CUDA下载地址](https://developer.nvidia.com/cuda-downloads)。如下图:
169 |
170 | 
171 |
172 | 这里我们选择了runfile选项,推荐使用这种方法安装。
173 |
174 | 
175 |
176 | 下载完成之后运行这个文件,执行:
177 |
178 | ~~~shell
179 | sudo sh cuda_8.0.61_375.26_linux.run
180 | ~~~
181 |
182 | 如果已经安装了显卡驱动,则不需要选择安装显卡驱动。一般建议这样做,cuda自带的驱动总会出一些问题。显卡驱动安装方法如下:
183 |
184 | ~~~shell
185 | sudo apt-get install nvidia-xxx # xxx 根据自己的驱动来
186 | ~~~
187 |
188 | 如果你选择了安装显卡驱动,这时候会有一些选项需要您手动选择,这个**很重要。**否则可能会使系统出现问题!!!
189 |
190 | 通常我们不需要安装OpenGL。
191 |
192 | 
193 |
194 |
195 |
196 | 安装完成之后需要添加环境变量。在`~/.bash_profile`、`~/.bashrc`等文件中(这里使用的是bash)的末尾添加下面的内容:
197 |
198 | ~~~shell
199 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64"
200 | export CUDA_HOME=/usr/local/cuda
201 | ~~~
202 |
203 | 或者执行如下命令:
204 |
205 | ~~~shell
206 | echo 'export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64"' >> ~/.bash_profile
207 | echo 'export CUDA_HOME=/usr/local/cuda' >> ~/.bash_profile
208 | ~~~
209 |
210 | 到此CUDA安装完毕。
211 |
212 | 安装完毕之后,可以重启终端或者执行如下命令,即可生效。
213 |
214 | ~~~shell
215 | source ~/.bash_profile
216 | ~~~
217 |
218 |
219 |
220 | ### 3. 下载并安装cuDNN
221 |
222 | 下载cuDNN需要注册。[CuDNN下载地址](https://developer.nvidia.com/rdp/cudnn-download)
223 |
224 | 这里我们使用1.3版本的Tensorflow,所以选择v6版本的cuDNN。之前的TF需要选择v5.1的cuDNN。
225 |
226 | 
227 |
228 | 然后选择对应的linux版本即可,这里我们推荐使用deb文件安装:
229 |
230 |
231 |
232 | 执行如下命令:
233 |
234 | ~~~shell
235 | sudo dpkg -i filename.deb
236 | ~~~
237 |
238 | 到此cuDNN安装完毕。
239 |
240 | *注意:也可以直接下载cuDNN,然后解压到CUDA的相应目录。*
241 |
242 | ###4. 安装GPU版Tensorflow
243 |
244 | 这里,我们依然可以选择使用pip安装或使用源码编译安装。
245 |
246 | 方法一:使用pip安装。
247 |
248 | ~~~shell
249 | pip install tensorflow-gpu
250 | ~~~
251 |
252 | 安装完毕。可以使用上面提到的验证方法验证。同样的这样安装无法完全发挥cpu的性能。可以使用编译安装。
253 |
254 |
255 |
256 | 方法二:编译安装。
257 |
258 | 编译安装与上面提到cpu版本的编译安装差不多,只有第6步,使用bazel构建pip包的命令稍有差异,如下:
259 |
260 | ~~~shell
261 | bazel build -c opt --copt=-march=native --config=cuda -k //tensorflow/tools/pip_package:build_pip_package
262 | ~~~
263 |
264 | 编译安装之后,可以利用上面提到的验证方法验证安装是否成功。
265 |
266 |
267 |
268 |
269 |
--------------------------------------------------------------------------------
/ch01/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/images/1.png
--------------------------------------------------------------------------------
/ch01/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/images/2.png
--------------------------------------------------------------------------------
/ch01/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/images/3.png
--------------------------------------------------------------------------------
/ch01/images/tree-def.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch01/images/tree-def.png
--------------------------------------------------------------------------------
/ch02/ch2-graph-and-sesseion.md:
--------------------------------------------------------------------------------
1 | Tensorflow使用**数据流图(Data Flow Graphs)**来定义计算流程。数据流图在定义阶段并不会执行出计算结果。数据流图使得计算的定义与执行分离开来。Tensorflow构建的数据流图是有向无环图。而图的执行需要在**会话(Session)**中完成。
2 |
3 | ## 1. 数据流图
4 |
5 | 我们尝试构建一个简单的图,这个图描述了两个数的加法运算,如下:
6 |
7 | ```python
8 | import tensorflow as tf
9 |
10 | a = tf.add(3, 5)
11 | ```
12 |
13 | 使用TensorBoard可视化图:
14 |
15 |
16 |
17 | 此处的x、y是被TensorFlow自动命名的,分别代表3与5这两个量。这里需要注意的是数据流图含了**边(edge)**和**节点(node)**,正好与Tensorflow中的tensor与flow对应。tensor代表了数据流图中的边,而flow这个动作代表了数据流图中的节点。节点也被称之为**操作(operation, op)**,一个 op 获得 0 个或多个 `Tensor`, 执行计算, 产生 0 个或多个 `Tensor`。 每个 Tensor 是一个类型化的多维数组 。关于节点与边,我们会在后续的内容当中详细描述。
18 |
19 | 这时候如果我们直接输出a,会得到如下结果:
20 |
21 | ```python
22 | print(a)
23 |
24 | >> Tensor("Add:0", shape=(), dtype=int32)
25 | ```
26 |
27 | 根据我们的推断,执行加法之后,应该得到一个值,为‘8’,然而并非如此。我们得到的是一个Tensor对象的描述。之所以如此是因为我们定义的变量a代表的是一个图的定义,这个图定义了一个加法运算,但并没有执行。要想得到计算结果,必须在会话中执行。
28 |
29 | ## 2. 会话
30 |
31 | 启动会话的第一步是创建一个session对象。会话提供在图中执行op的一些列方法。一般的模式是,建立会话,在会话中添加图,然后执行。建立会话常用两种方式,一种是`tf.Session()`,通常使用这种方法创建会话;另一种是`tf.InteractiveSession()`,这种方式更多的用在iPython notebook等交互式Python环境中。
32 |
33 | 在会话中执行图:
34 |
35 | ```python
36 | import tensorflow as tf
37 |
38 | # 创建图
39 | a = tf.add(3, 5)
40 | # 创建会话
41 | sess = tf.Session()
42 | # 执行图
43 | res = sess.run(a) # print(res) 可以得到 8
44 | # 关闭会话
45 | sess.close()
46 | ```
47 |
48 | 为了简便,我们可以使用上下文管理器来创建Session,所以也可以写成:
49 |
50 | ```python
51 | a = tf.add(3, 5)
52 | with tf.Session() as sess:
53 | print(sess.run(a))
54 | ```
55 |
56 | ### 2.1 feed与fetch
57 |
58 | 在调用Session对象的run()方法来执行图时,传入一些张量,这一过程叫做**填充(feed)**,返回的结果类型根据输入的类型而定,这个过程叫**取回(fetch)**。
59 |
60 | `tf.Session.run()`方法可以运行并评估传入的张量:
61 |
62 | ```python
63 | # 运行fetches中的所有张量
64 | run(fetches, feed_dict=None, options=None, run_metadata=None)
65 | ```
66 |
67 | 除了使用`tf.Session.run()`以外,在`sess`持有的上下文中还可以使用`eval()`方法。
68 |
69 | ```python
70 | a = tf.add(3, 5)
71 | with tf.Session() as sess:
72 | print(a.eval())
73 | ```
74 |
75 | 这里需要注意,`a.eval()`必须在`sess`持有的上下文中执行才可行。有时候,在交互式的Python环境中,上下文管理器使用不方便,这时候我们可以使用交互式会话来代替会话,这样`eval()`方法便可以随时使用:
76 |
77 | ```python
78 | a = tf.add(3, 5)
79 | sess = tf.InteractiveSession()
80 | print(a.eval())
81 | sess.close()
82 | ```
83 |
84 | 两种会话的区别可以通过查看是否支持直接使用`eval()`进行区分。
85 |
86 | **注意**:当我们构建一个加法运算的图,如`tf.add(3, 5)`时,这时候如果打印其返回值,可以看到返回值为一个`Tensor`对象,而将`Tensor`对象填充进入会话中执行之后,取回的值并不是`Tensor`对象,而是`numpy`下的`ndarray`对象。这是因为`TensorFlow`底层使用了`numpy`作为科学计算核心,而`Tensor`对象仅仅是一个符号,没有具体的数值,在会话中执行完成之后返回的`ndarray`对象是包含具体值的。
87 |
88 | ### 2.2 节点依赖
89 |
90 | 通常一个图会有较多的边与节点,这时候在会话中执行图时,所有依赖的节点均参与计算,如:
91 |
92 | ```python
93 | import tensorflow as tf
94 |
95 | x = 2
96 | y = 3
97 |
98 | op1 = tf.add(x, y)
99 | op2 = tf.multiply(x, y)
100 | op3 = tf.pow(op2, op1)
101 |
102 | with tf.Session() as sess:
103 | res = sess.run(op3)
104 | ```
105 |
106 | 利用TensorBoard可视化图,如下:
107 |
108 |
109 |
110 | 虽然`session`中运行的是节点`op3`,然而与之相关联的`op1`、`op2`也参与了运算。
111 |
112 | **注意**:会话中也可以定义操作,但不推荐在会话中构建图的主要部分,这会使得代码混乱。同时,由于在会话中往往有大量循环语句,例如循环训练模型,这时候如果在循环语句中加入了操作会使得图的操作越来越多,占用内存越来越大,甚至程序崩溃。
113 |
114 | ------
115 |
116 | **小练习**:
117 |
118 | 构建一个图,描述从1到100的累加,并在会话中执行得到结果。然后描述图的形式,并简要画出从1到3的累加的图的形式。
119 |
120 | ## 3. 子图
121 |
122 | 图的构建可以是多种多样的,以上构建的图中数据只有一个流动方向,事实上我们可以构建多个通路的图,每一个通路可以称之为其子图。如下:
123 |
124 | ```python
125 | import tensorflow as tf
126 |
127 | x = 2
128 | y = 3
129 |
130 | add_op = tf.add(x, y)
131 | mul_op = tf.multiply(x, y)
132 |
133 | useless = tf.mul(x, add_op)
134 | pow_op = tf.pow(add_op, mul_op)
135 |
136 | with tf.Session() as sess:
137 | res = sess.run(pow_op)
138 | ```
139 |
140 | 利用TensorBoard可视化图:
141 |
142 |
143 |
144 | 可以看到此图中有两个子图,即运行完整的图我们可以得到两个结果`useless`和`pow_op`。当我们执行上述代码时,相当于执行了一个子图`pow_op`。这里需要注意由于得到`pow_op`用不到`useless`的值,即没有依赖关系,所以`useless`这个node并没有执行,如果我们需要得到`useless`和`pow_op`两个节点的值,则需要稍微改进代码:
145 |
146 | ```python
147 | import tensorflow as tf
148 |
149 | x = 2
150 | y = 3
151 |
152 | add_op = tf.add(x, y)
153 | mul_op = tf.multiply(x, y)
154 |
155 | useless = tf.multiply(x, add_op)
156 | pow_op = tf.pow(add_op, mul_op)
157 |
158 | with tf.Session() as sess:
159 | res1, res2 = sess.run([pow_op, useless])
160 | ```
161 |
162 | ## 4. 多个图
163 |
164 | 一个图包含了一组op对象和张量,描述了一个计算流程。但有时候我们需要建构多个图,在不同的图中定义不同的计算,例如一个图描述了算法的训练,另一个图描述了这个算法的正常运行过程,这时候需要在不同的会话中调用不同的图。
165 |
166 | 创建图使用`tf.Graph()`。将图设置为默认图使用`tf.Graph.as_default()`,此方法会返回一个上下文管理器。如果不显示的添加一个默认图,系统会自动设置一个全局的默认图。所设置的默认图,在模块范围内定义的节点都将自动加入默认图中。
167 |
168 | ```python
169 | # 构建一个图
170 | g = tf.Graph()
171 | # 将此图作为默认图
172 | with g.as_default():
173 | op = tf.add(3, 5)
174 | ```
175 |
176 | 上述代码也可以写成:
177 |
178 | ```python
179 | with tf.Graph().as_default():
180 | op = tf.add(3, 5)
181 | ```
182 |
183 | `tf.Graph()`*不能用作为上下文管理器,必须调用其方法*`as_default()`。
184 |
185 | 构建多个图:
186 |
187 | ```python
188 | g1 = tf.Graph()
189 | with g1.as_default():
190 | pass
191 |
192 | g2 = tf.Graph()
193 | with g2.as_default():
194 | pass
195 | ```
196 |
197 | 在我们加载了Tensorflow包时,就已经加载了一个图,这也是我们可以不创建图而直接构建边和节点的原因,这些边与节点均加入了默认图。我们可以使用`tf.get_default_graph()`方法获取到当前环境中的图,如下:
198 |
199 | ```python
200 | # 获取到了系统的默认图
201 | g1 = tf.get_default_graph()
202 | with g1.as_default():
203 | pass
204 |
205 | g2 = tf.Graph()
206 | with g2.as_default():
207 | pass
208 | ```
209 |
210 | `tf.get_default_graph()`获取到的图时当前所在图的环境。如果在上面的代码中在`g2`的图环境中执行`tf.get_default_graph()`,则得到的图是`g2`。
211 |
212 | *在使用notebook时,由于notebook的一些特性,通常需要指定默认图,否则程序可能报错。为了书写规范,推荐尽量书写完整默认图。*
213 |
214 | 当使用了多个图时,在`session`中就需要指定当前`session`所运行的图,不指定时,为默认图。如下:
215 |
216 | ```python
217 | # 指定当前会话处理g这个图
218 | with tf.Session(graph=g) as sess:
219 | sess.run(op)
220 | ```
221 |
222 | ------
223 |
224 | **小练习**
225 |
226 | 画出如下代码对应的数据流图,并说明包含几个图,以及区分`add_op`是图还是子图。
227 |
228 | ```python
229 | import tensorflow as tf
230 |
231 | g1 = tf.get_default_graph()
232 | with g1.as_default():
233 | x = 3
234 | y = 5
235 | add_op = tf.add(x, y)
236 |
237 | x_2 = 3
238 | y_2 = 5
239 | mul_op = tf.multiply(x_2, y_2)
240 |
241 | g2 = tf.Graph()
242 | with g2.as_default():
243 | pass
244 | ```
245 |
246 | ## 5. 单机多卡计算
247 |
248 | 当我们有多个计算设备时,可以将一个计算图的不同子图分别使用不同的计算设备进行运算,以提高运行速度。一般,我们不需要显示的指定使用CPU或者GPU进行计算,Tensorflow能自动检测,如果检测到GPU,Tensorflow会尽可能的利用找到的第一个GPU进行操作。如果机器上有超过一个可用的GPU,出第一个外,其它的GPU默认是不参与计算的。为了让Tensorflow使用这些GPU,我们必须明确指定使用何种设备进行计算。
249 |
250 | TensorFlow用指定字符串 `strings` 来标识这些设备. 比如:
251 |
252 | - `"/cpu:0"`: 机器中的 CPU
253 | - `"/gpu:0"`: 机器中的 GPU, 如果你有一个的话.
254 | - `"/gpu:1"`: 机器中的第二个 GPU, 以此类推...
255 |
256 | 这里需要注意的是,CPU可以支持所有类型的运行,而GPU只支持部分运算,例如矩阵乘法。下面是一个使用第3个GPU进行矩阵乘法的运算:
257 |
258 | ```python
259 | with tf.device('/gpu:2'):
260 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
261 | b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='b')
262 | c = tf.matmul(a, b)
263 | ```
264 |
265 | 为了查看节点和边被指派给哪个设备运算,可以在`session`中配置“记录设备指派”为开启,如下:
266 |
267 | ```python
268 | with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess:
269 | print(sess.run(c))
270 | ```
271 |
272 | 当指定的设备不存在时,会出现 `InvalidArgumentError` 错误提示。为了避免指定的设备不存在这种情况,可以在创建的 `session` 里把参数 `allow_soft_placement` 设置为 `True`,这时候TF会在设备不存在时自动分配设备。如下:
273 |
274 | ```python
275 | config = tf.ConfigProto(
276 | log_device_placement=True,
277 | allow_soft_placement=True)
278 | with tf.Session(config=config) as sess:
279 | pass
280 | ```
281 |
282 | ------
283 |
284 | **小练习**:
285 |
286 | 尝试为不同设备分配不同计算节点,并打印出设备信息。
287 |
288 | ## 6. 使用图与会话的优点
289 |
290 | 对于图与会话:
291 |
292 | - 多个图需要多个会话来运行,每个会话会尝试使用所有可用的资源。
293 | - 不同图、不同会话之间无法直接通信。
294 | - 最好在一个图中有多个不连贯的子图。
295 |
296 | 使用图的优点:
297 |
298 | - 节省计算资源。我们仅仅只需要计算用到的子图就可以了,无需完整计算整个图。
299 | - 可以将复杂计算分成小块,进行自动微分等运算。
300 | - 促进分布式计算,将工作分布在多个CPU、GPU等设备上。
301 | - 很多机器学习模型可以使用有向图进行表示,与计算图一致。
302 |
303 | ## 作业
304 |
305 | 1. 在一个notebook文件中构建一张图,实现两个数的加法操作,并在两个不同的会话中执行图。
306 | 2. 查找资料学习TensorFlow中执行常量基本运算API的基本用法,如`tf.add`、`tf.subtract`、`tf.multiply`、`tf.divide`、`tf.mod`、`tf.pow`、`tf.square`、`tf.sqrt`等的用法,并在notebook中演示其基本用法。
307 |
308 |
--------------------------------------------------------------------------------
/ch03/ch3-edge-and-node.md:
--------------------------------------------------------------------------------
1 | 图包含了节点与边。边可以看做是流动着的数据以及相关依赖关系。而节点表示了一种操作,即对当前流动来的数据的运算。
2 |
3 | 需要说明的是,诸如`tf.add`这样的操作,我们可以看做是一个节点,然而其返回值往往并不代表这个节点,而只代表这个节点输出的张量。当我们使用如`c = tf.add(a, b)`的代码时需要知道`c`一般代表的是张量,而不是操作(也有极少数例外),当然也有一些方法可以获得操作,后面的教程会加以说明。
4 |
5 | ## 1. 边(edge)
6 |
7 | Tensorflow的边有两种连接关系:**数据依赖**和**控制依赖**。其中,实线边表示数据依赖,代表数据,即张量。虚线边表示控制依赖(control dependency),可用于控制操作的运行,这被用来确保happens-before关系,这类边上没有数据流过,但源节点必须在目的节点开始执行前完成执行。
8 |
9 | ### 1.1 数据依赖
10 |
11 | 数据依赖很容易理解,某个节点会依赖于其它节点的数据,如下所示,矩阵乘法操作这个节点依赖于`a、b`的数据才能执行:
12 |
13 | ```python
14 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
15 | b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='b')
16 | c = tf.matmul(a, b)
17 | ```
18 |
19 | 当节点关系比较复杂时,如下:
20 |
21 | ```python
22 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
23 | b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='b')
24 | c = tf.matmul(a, b)
25 | d = tf.add(c, 5)
26 | ```
27 |
28 | 此时如果要执行并得到`d`中的数值,则只需在会话中执行`d`即可,TensorFlow会根据数据的依赖关系执行得到`c`的操作。
29 |
30 | ### 1.2 控制依赖
31 |
32 | 控制依赖是在某些操作之间没有数值上的依赖关系但执行时又需要使这些操作按照一定顺序执行,这时候我们可以声明执行顺序。这在TensorFlow包含变量相关操作时非常常用。
33 |
34 | 控制依赖使用**图对象的方法**`tf.Graph.control_dependencies(control_inputs)`,返回一个上下文管理器对象,用法如下:
35 |
36 | ```python
37 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
38 | b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='b')
39 | c = tf.matmul(a, b)
40 |
41 | g = tf.get_default_graph()
42 | with g.control_dependencies([c]):
43 | d = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='d')
44 | e = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='e')
45 | f = tf.matmul(d, e)
46 | with tf.Session() as sess:
47 | sess.run(f)
48 | ```
49 |
50 | 上面的例子中,我们在会话中执行了`f`这个节点,可以看到其与`c`这个节点并无任何数据依赖关系,然而`f`这个节点必须等待`c`这个节点执行完成才能够执行`f`。最终的结果是`c`先执行,`f`再执行。
51 |
52 | **注意**:`control_dependencies`方法传入的是一个列表作为参数,列表中包含所有被依赖的操作或张量,被依赖的所有节点可以看做是同时执行的。
53 |
54 | 控制依赖除了上面的写法以外还拥有简便的写法(推荐使用):`tf.control_dependencies(control_inputs)`。其调用默认图的`tf.Graph.control_dependencies(control_inputs)`方法。上面的写法等价于:
55 |
56 | ```python
57 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
58 | b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='b')
59 | c = tf.matmul(a, b)
60 |
61 | with tf.control_dependencies([c]):
62 | d = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='d')
63 | e = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], name='e')
64 | f = tf.matmul(d, e)
65 | with tf.Session() as sess:
66 | sess.run(f)
67 | ```
68 |
69 | **注意**:有依赖的op必须写在`tf.control_dependencies`上下文中,否则不属于有依赖的op。**如下写法是错误的**:
70 |
71 | ```python
72 | def my_fun():
73 | a = tf.constant(1)
74 | b = tf.constant(2)
75 | c = a + b
76 |
77 | d = tf.constant(3)
78 | e = tf.constant(4)
79 | f = d + e
80 | # 此处 f 不依赖于 c
81 | with tf.control_dependencies([c]):
82 | return f
83 |
84 | result = my_fun()
85 | ```
86 |
87 | ### 1.3 张量的阶、形状、数据类型
88 |
89 | Tensorflow数据流图中的边用于数据传输时,数据是以张量的形式传递的。张量有阶、形状和数据类型等属性。
90 |
91 | #### Tensor的阶
92 |
93 | 在TensorFlow系统中,张量的维数被描述为**阶**。但是张量的阶和矩阵的阶并不是同一个概念。张量的阶是张量维数的一个数量描述。比如,下面的张量(使用Python中list定义的)就是2阶.
94 |
95 | ```python
96 | t = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
97 | ```
98 |
99 | 你可以认为一个二阶张量就是我们平常所说的矩阵,一阶张量可以认为是一个向量.对于一个二阶张量你可以用语句`t[i, j]`来访问其中的任何元素.而对于三阶张量你可以用`t[i, j, k]`来访问其中的任何元素。
100 |
101 | | 阶 | 数学实例 | Python 例子 |
102 | | ---- | ----------------------- | ------------------------------------------------------------ |
103 | | 0 | 纯量 (或标量。只有大小) | `s = 483` |
104 | | 1 | 向量(大小和方向) | `v = [1.1, 2.2, 3.3]` |
105 | | 2 | 矩阵(数据表) | `m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]` |
106 | | 3 | 3阶张量 (数据立体) | `t = [[[2], [4], [6]], [[8], [10], [12]], [[14], [16], [18]]]` |
107 | | n | n阶 | `....` |
108 |
109 | #### Tensor的形状
110 |
111 | TensorFlow文档中使用了三种记号来方便地描述张量的维度:阶,形状以及维数.下表展示了他们之间的关系:
112 |
113 | | 阶 | 形状 | 维数 | 实例 |
114 | | ---- | ---------------- | ---- | ----------------------------------- |
115 | | 0 | [ ] | 0-D | 一个 0维张量. 一个纯量. |
116 | | 1 | [D0] | 1-D | 一个1维张量的形式[5]. |
117 | | 2 | [D0, D1] | 2-D | 一个2维张量的形式[3, 4]. |
118 | | 3 | [D0, D1, D2] | 3-D | 一个3维张量的形式 [1, 4, 3]. |
119 | | n | [D0, D1, … Dn-1] | n-D | 一个n维张量的形式 [D0, D1, … Dn-1]. |
120 |
121 | 张量的阶可以使用`tf.rank()`获取到:
122 |
123 | ```python
124 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
125 | tf.rank(a) # => 2
126 | ```
127 |
128 | 张量的形状可以通过Python中的列表或元祖(list或tuples)来表示,或者也可用`TensorShape`对象来表示。如下:
129 |
130 | ```python
131 | # 指定shape是[2, 3]的常量,这里使用了list指定了shape,也可以使用ndarray和TensorShape对象来指定shape
132 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], shape=[2, 3])
133 |
134 | # 获取shape 方法一:利用tensor的shape属性
135 | a.shape # TensorShape([Dimension(2), Dimension(3)])
136 |
137 | # 获取shape 方法二:利用Tensor的方法get_shape()
138 | a.get_shape() # TensorShape([Dimension(2), Dimension(3)])
139 |
140 | # 获取shape 方法三:利用tf.shape(),返回一个Tensor对象
141 | tf.shape(a) #
142 | ```
143 |
144 | 注意:在动态图中要是有方法三获取`shape`。
145 |
146 | `TensorShape`对象有一个方法`as_list()`,可以将`TensorShape`对象转化为python的list对象。
147 |
148 | ```python
149 | a.get_shape().as_list() # [2, 3]
150 | ```
151 |
152 | 同样的我们也可以使用list构建一个TensorShape的对象:
153 |
154 | ```python
155 | ts = tf.TensorShape([2, 3])
156 | ```
157 |
158 | ------
159 |
160 | **小练习**:
161 |
162 | 1. 写出如下张量的阶与形状,并使用TensorFlow编程验证:
163 |
164 | - `[]`
165 | - `12`
166 | - `[[1], [2], [3]]`
167 | - `[[1, 2, 3], [1, 2, 3]]`
168 | - `[[[1]], [[2]], [[3]]]`
169 | - `[[[1, 2], [1, 2]]]`
170 |
171 | 1. 设计一个函数,要求实现:可以根据输入张量输出shape完成一样的元素为全1的张量。提示:使用`tf.ones`函数可根据形状生成全1张量。
172 |
173 | 小技巧:使用`tf.ones_like`、`tf.zeros_like`可以快速创建与输入张量形状一样的全1或全0张量。
174 |
175 | #### Tensor的数据类型
176 |
177 | 每个Tensor均有一个数据类型属性,用来描述其数据类型。合法的数据类型包括:
178 |
179 | | 数据类型 | Python 类型 | 描述 |
180 | | --------------- | --------------- | ----------------------------------------------------------- |
181 | | `DT_FLOAT` | `tf.float32` | 32 位浮点数. |
182 | | `DT_DOUBLE` | `tf.float64` | 64 位浮点数. |
183 | | `DT_INT64` | `tf.int64` | 64 位有符号整型. |
184 | | `DT_INT32` | `tf.int32` | 32 位有符号整型. |
185 | | `DT_INT16` | `tf.int16` | 16 位有符号整型. |
186 | | `DT_INT8` | `tf.int8` | 8 位有符号整型.(此处符号位不算在数值位当中) |
187 | | `DT_UINT8` | `tf.uint8` | 8 位无符号整型. |
188 | | `DT_STRING` | `tf.string` | 可变长度的字节数组.每一个张量元素都是一个字节数组. |
189 | | `DT_BOOL` | `tf.bool` | 布尔型.(不能使用number类型表示bool类型,但可转换为bool类型) |
190 | | `DT_COMPLEX64` | `tf.complex64` | 由两个32位浮点数组成的复数:实部和虚部。 |
191 | | `DT_COMPLEX128` | `tf.complex128` | 由两个64位浮点数组成的复数:实部和虚部。 |
192 | | `DT_QINT32` | `tf.qint32` | 用于量化Ops的32位有符号整型. |
193 | | `DT_QINT8` | `tf.qint8` | 用于量化Ops的8位有符号整型. |
194 | | `DT_QUINT8` | `tf.quint8` | 用于量化Ops的8位无符号整型. |
195 |
196 | Tensor的数据类型类似于Numpy中的数据类型,但其加入了对string的支持。
197 |
198 | ##### 设置与获取Tensor的数据类型
199 |
200 | 设置Tensor的数据类型:
201 |
202 | ```python
203 | # 方法一
204 | # Tensorflow会推断出类型为tf.float32
205 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
206 |
207 | # 方法二
208 | # 手动设置
209 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=tf.float32)
210 |
211 | # 方法三 (不推荐)
212 | # 设置numpy类型 未来可能会不兼容
213 | # tf.int32 == np.int32 -> True
214 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32)
215 | ```
216 |
217 | 获取Tensor的数据类型,可以使用如下方法:
218 |
219 | ```python
220 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
221 | a.dtype # tf.float32
222 | print(a.dtype) # >>
223 |
224 | b = tf.constant(2+3j) # tf.complex128 等价于 tf.complex(2., 3.)
225 | print(b.dtype) # >>
226 |
227 | c = tf.constant([True, False], tf.bool)
228 | print(c.dtype) #
229 | ```
230 |
231 | 这里需要注意的是一个张量仅允许一种dtype存在,也就是一个张量中每一个数据的数据类型必须一致。
232 |
233 | ##### 数据类型转化
234 |
235 | 如果我们需要将一种数据类型转化为另一种数据类型,需要使用`tf.cast()`进行:
236 |
237 | ```python
238 | a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], name='a')
239 | # tf.cast(x, dtype, name=None) 通常用来在两种数值类型之间互转
240 | b = tf.cast(a, tf.int16)
241 | print(b.dtype) # >>
242 | ```
243 |
244 | 有些类型利用`tf.cast()`是无法互转的,比如string无法转化成为number类型,这时候可以使用以下方法:
245 |
246 | ```python
247 | # 将string转化为number类型 注意:数字字符可以转化为数字
248 | # tf.string_to_number(string_tensor, out_type = None, name = None)
249 | a = tf.constant([['1.0', '2.0', '3.0'], ['4.0', '5.0', '6.0']], name='a')
250 | num = tf.string_to_number(a)
251 | ```
252 |
253 | 实数数值类型可以使用`tf.cast`方法转化为bool类型。
254 |
255 | ------
256 |
257 | **小练习**
258 |
259 | 判断以下哪个张量是合法的:
260 |
261 | - `tf.constant([1, 2, 3], dtype=tf.float64)`
262 | - `tf.constant([1, 2, 3], dtype=tf.complex64)`
263 | - `tf.constant([1, 2, 3], dtype=tf.string)`
264 | - `tf.consant([1, '2', '3'])`
265 | - `tf.constant([1, [2, 3]])`
266 |
267 | ## 2. 节点
268 |
269 | 图中的节点也可以称之为**算子**,它代表一个操作(operation, OP),一般用来表示数学运算,也可以表示数据输入(feed in)的起点以及输出(push out)的终点,或者是读取/写入持久变量(persistent variable)的终点。常见的节点包括以下几种类型:变量、张量逐元素运算、张量变形、张量索引与切片、张量运算、检查点操作、队列和同步操作、张量控制等。
270 |
271 | 当OP表示数学运算时,每一个运算都会创建一个`tf.Operation`对象。常见的操作,例如生成一个变量或者常量、数值计算均创建`tf.Operation`对象
272 |
273 | ### 2.1 变量
274 |
275 | 变量用于存储张量,可以使用list、Tensor等来进行初始化,例如:
276 |
277 | ```python
278 | # 使用纯量0进行初始化一个变量
279 | var = tf.Variable(0)
280 | ```
281 |
282 | ### 2.2 张量元素运算
283 |
284 | 常见张量元素运算有很多种,比如张量对应元素的相加、相乘等,这里我们介绍以下几种运算:
285 |
286 | - `tf.add()` 两个张量对应元素相加。等价于`A + B`。
287 |
288 | ```python
289 | tf.add(1, 2) # 3
290 | tf.add([1, 2], [3, 4]) # [4, 6]
291 | tf.constant([1, 2]) + tf.constant([3, 4]) # [4, 6]
292 | ```
293 |
294 | - `tf.subtract()` 两个张量对应元素相减。等价于`A - B`。
295 |
296 | ```python
297 | tf.subtract(1, 2) # -1
298 | tf.subtract([1, 2], [3, 4]) # [-2, -2]
299 | tf.constant([1, 2]) - tf.constant([3, 4]) # [-2, -2]
300 | ```
301 |
302 | - `tf.multiply()` 两个张量对应元素相乘。等价于`A * B`。
303 |
304 | ```python
305 | tf.multiply(1, 2) # 2
306 | tf.multiply([1, 2], [3, 4]) # [3, 8]
307 | tf.constant([1, 2]) * tf.constant([3, 4]) # [3, 8]
308 | ```
309 |
310 | - `tf.scalar_mul() `一个纯量分别与张量中每一个元素相乘。等价于 `a * B`
311 |
312 | ```python
313 | sess.run(tf.scalar_mul(10., tf.constant([1., 2.]))) # [10., 20.]
314 | ```
315 |
316 | - `tf.divide()` 两个张量对应元素相除。等价于`A / B`。这个除法操作是Tensorflow推荐使用的方法。此方法不接受Python自身的数据结构,例如常量或list等。
317 |
318 | ```python
319 | tf.divide(1, 2) # 0.5
320 | tf.divide(tf.constant([1, 2]), tf.constant([3, 4])) # [0.33333333, 0.5]
321 | tf.constant([1, 2]) / tf.constant([3, 4]) # [0.33333333, 0.5]
322 | ```
323 |
324 | - `tf.div()`两个张量对应元素相除,得到的结果。不推荐使用此方法。`tf.divide`与`tf.div`相比,`tf.divide`符合Python的语义。例如:
325 |
326 | ```python
327 | 1/2 # 0.5
328 | tf.divide(tf.constant(1), tf.constant(2)) # 0.5
329 | tf.div(1/2) # 0
330 | ```
331 |
332 | - `tf.floordiv()` shape相同的两个张量对应元素相除取整数部分。等价于`A // B`。
333 |
334 | ```python
335 | tf.floordiv(1, 2) # 0
336 | tf.floordiv([4, 3], [2, 5]) # [2, 0]
337 | tf.constant([4, 3]) // tf.constant([2, 5]) # [2, 0]
338 | ```
339 |
340 | - `tf.mod()` shape相同的两个张量对应元素进行模运算。等价于`A % B`。
341 |
342 | ```python
343 | tf.mod([4, 3], [2, 5]) # [0, 3]
344 | tf.constant([4, 3]) % tf.constant([2, 5]) # [0, 3]
345 | ```
346 |
347 | 上述运算也支持满足一定条件的`shape`不同的两个张量进行运算,即广播运算。与`numpy`类似在此不做过多演示。
348 |
349 | 除此以外还有很多的逐元素操作的函数,例如求平方`tf.square()`、开平方`tf.sqrt`、指数运算、三角函数运算、对数运算等等。
350 |
351 | ### 2.3 张量常用运算
352 |
353 | `tf.matmul()` 通常用来做矩阵乘法。
354 |
355 | `tf.transpose()` 转置张量。
356 |
357 | ```python
358 | a = tf.constant([[1., 2., 3.], [4., 5., 6.0]])
359 | # tf.matmul(a, b, transpose_a=False, transpose_b=False, adjoint_a=False, adjoint_b=False, a_is_sparse=False, b_is_sparse=False, name=None)
360 | # tf.transpose(a, perm=None, name='transpose')
361 | tf.matmul(a, tf.transpose(a)) # 等价于 tf.matmul(a, a, transpose_b=True)
362 | ```
363 |
364 | 更多线性代数操作在`tf.linalg`模块下。
365 |
366 | ### 2.4 张量切片与索引
367 |
368 | 张量变形:
369 |
370 | ```python
371 | # 将张量变为指定shape的新张量
372 | # tf.reshape(tensor, shape, name=None)
373 | # tensor 't' is [1, 2, 3, 4, 5, 6, 7, 8, 9]
374 | # tensor 't' has shape [9]
375 | new_t = tf.reshape(t, [3, 3])
376 | # new_t ==> [[1, 2, 3],
377 | # [4, 5, 6],
378 | # [7, 8, 9]]
379 | new_t = tf.reshape(new_t, [-1]) # 这里需要注意shape是一阶张量,此处不能直接使用 -1
380 | # tensor 'new_t' is [1, 2, 3, 4, 5, 6, 7, 8, 9]
381 | ```
382 |
383 | 张量的拼接:
384 |
385 | ```python
386 | # 沿着某个维度对二个或多个张量进行连接
387 | # tf.concat(values, axis, name='concat')
388 | t1 = [[1, 2, 3], [4, 5, 6]]
389 | t2 = [[7, 8, 9], [10, 11, 12]]
390 | tf.concat([t1, t2], 0) ==> [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
391 | ```
392 |
393 | 张量的切割:
394 |
395 | ```python
396 | # 对输入的张量进行切片
397 | # tf.slice(input_, begin, size, name=None)
398 | # 'input' is [[[1, 1, 1], [2, 2, 2]],
399 | # [[3, 3, 3], [4, 4, 4]],
400 | # [[5, 5, 5], [6, 6, 6]]]
401 | tf.slice(input, [1, 0, 0], [1, 1, 3]) ==> [[[3, 3, 3]]]
402 | tf.slice(input, [1, 0, 0], [1, 2, 3]) ==> [[[3, 3, 3],
403 | [4, 4, 4]]]
404 | tf.slice(input, [1, 0, 0], [2, 1, 3]) ==> [[[3, 3, 3]],
405 | [[5, 5, 5]]]
406 |
407 | # 将张量分裂成子张量
408 | # tf.split(value, num_or_size_splits, axis=0, num=None, name='split')
409 | # 'value' is a tensor with shape [5, 30]
410 | # Split 'value' into 3 tensors with sizes [4, 15, 11] along dimension 1
411 | split0, split1, split2 = tf.split(value, [4, 15, 11], 1)
412 | tf.shape(split0) ==> [5, 4]
413 | tf.shape(split1) ==> [5, 15]
414 | tf.shape(split2) ==> [5, 11]
415 | ```
416 |
417 | ------
418 |
419 | **小练习**:
420 |
421 | 已知一张28\*28像素图片存储在一阶张量中(从左到右、从上到下逐行展开的),请问:
422 |
423 | 1. 一阶张量的形状是多少?
424 | 2. 将一阶张量还原回图片格式的二阶张量。
425 | 3. 取图片张量中第5-10行、第3列(索引坐标从0开始)的数据。
426 |
427 | ## 作业
428 |
429 | 有一4阶张量`img`其`shape=[10, 28, 28, 3])`,代表10张28*28像素的3通道RGB图像,问:
430 |
431 | 1. 如何利用索引取出第2张图片?(注意:索引均从0开始,第二张则索引为1,下同)
432 | 2. 如何利用切片取出第2张图片?
433 | 3. 使用切片与使用索引取出的一张图片有何不同?
434 | 4. 如何取出其中的第1、3、5、7张图片?
435 | 5. 如何取出第6-8张(包括6不包括8)图片中中心区域(14*14)的部分?
436 | 6. 如何将图片根据通道拆分成三份单通道图片?
437 | 7. 写出`tf.shape(img)`返回的张量的阶数以及`shape`属性的值。
438 |
439 |
--------------------------------------------------------------------------------
/ch04/ch4-constant-variable-placeholder.md:
--------------------------------------------------------------------------------
1 | TensorFlow用图来完成运算流程的描述。一个图是由OP与Tensor构成,即通过OP生成、消费或改变Tensor。TensorFlow虽然并非是一门编程语言,而是一种符号式编程库,但是因为需要将各种各样的操作符号化,并在C++环境中执行,这时就会出现很多类似于编程语言属性的符号函数与类,事实上TensorFlow包含了常量、变量、数学操作符、流程操作符等看起来很像编程语言属性的内容。
2 |
3 | ## 1. 常量
4 |
5 | 常量是一块只读的内存区域,常量在**初始化时就必须赋值**,并且之后值将不能被改变。Python并无内置常量关键字,需要用到时往往需要我们去实现,而Tensorflow内置了常量方法 `tf.constant()`。
6 |
7 | ### 1.1 普通常量
8 |
9 | 普通常量使用`tf.constant()`初始化得到,其有5个参数。
10 |
11 | ```python
12 | constant(
13 | value,
14 | dtype=None,
15 | shape=None,
16 | name="Const",
17 | verify_shape=False):
18 | ```
19 |
20 | - `value`是必填参数,即常量的初识值。这里需要注意,这个`value`可以是Python中的`list`、`tuple`以及`Numpy`中的`ndarray`对象,但**不可以是Tensor对象**,因为这样没有意义。
21 | - `dtype`可选参数,表示数据类型,`value`中的数据类型应与与`dtype`中的类型一致,如果不填写则会根据`value`中值的类型进行推断。
22 | - `shape` 可选参数,表示value的形状。如果参数`verify_shape=False`,`shape`在与`value`形状不一致时会修改`value`的形状。如果参数`verify_shape=True`,则要求`shape`必须与`value`的`shape`一致。当`shape`不填写时,默认为`value`的`shape`。
23 |
24 | **注意**:`tf.constant()`生成的是一个张量。其类型是`tf.Tensor`。
25 |
26 | ### 1.2 常量存储位置
27 |
28 | **常量存储在图的定义当中**,可以将图序列化后进行查看:
29 |
30 | ```python
31 | const_a = tf.constant([1, 2])
32 | with tf.Session() as sess:
33 | # tf.Graph.as_graph_def 返回一个代表当前图的序列化的`GraphDef`
34 | print(sess.graph.as_graph_def()) # 你将能够看到const_a的值
35 | ```
36 |
37 | 当常量包含的数据量较大时,会影响图的加载速度。通常较大的数据使用变量或者在图加载完成之后读取。
38 |
39 | ### 1.3 序列常量
40 |
41 | 除了使用`tf.constant()`生成任意常量以外,我们还可以使用一些方法快捷的生成**序列常量**:
42 |
43 | ```python
44 | # 在指定区间内生成均匀间隔的数字
45 | tf.linspace(start, stop, num, name=None) # slightly different from np.linspace
46 | tf.linspace(10.0, 13.0, 4) ==> [10.0 11.0 12.0 13.0]
47 |
48 | # 在指定区间内生成均匀间隔的数字 类似于python中的range
49 | tf.range(start, limit=None, delta=1, dtype=None, name='range')
50 | # 例如:'start' is 3, 'limit' is 18, 'delta' is 3
51 | tf.range(start=3, limit=18, delta=3) ==> [3, 6, 9, 12, 15]
52 | # 例如:'limit' is 5
53 | tf.range(limit) ==> [0, 1, 2, 3, 4]
54 | ```
55 |
56 | **注意**:`tf.range`中,当传入一个默认参数时,代表的是设置`limit`,此时`start`默认为0。
57 |
58 | ### 1.4 随机数常量
59 |
60 | 类似于Python中的`random`模块,Tensorflow也拥有自己的随机数生成方法。可以生成**随机数常量**:
61 |
62 | ```python
63 | # 生成服从正态分布的随机数
64 | tf.random_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
65 |
66 | # 生成服从截断的正态分布的随机数
67 | # 只保留了两个标准差以内的值,超出的值会被丢掉重新生成
68 | tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None,
69 | name=None)
70 |
71 | # 生成服从均匀分布的随机值
72 | tf.random_uniform(shape, minval=0, maxval=None, dtype=tf.float32, seed=None,
73 | name=None)
74 |
75 | # 将输入张量在第一个维度上进行随机打乱
76 | tf.random_shuffle(value, seed=None, name=None)
77 |
78 | # 随机的将张量收缩到给定的尺寸
79 | # 注意:不是打乱,是随机的在某个位置开始裁剪指定大小的样本
80 | # 可以利用样本生成子样本
81 | tf.random_crop(value, size, seed=None, name=None)
82 | ```
83 |
84 | #### 1.4.1 随机数种子
85 |
86 | 随机数常量的生成依赖于两个随机数种子,一个是图级别的种子,另一个是操作级别的种子。上述所有操作当中,每个操作均可以接收一个`seed`参数,这个参数可以是任意一个整数,即为操作级种子。图级种子使用`tf.set_random_seed()`进行设置。
87 |
88 | **注意**:每个随机数种子可以确定一个随机数序列,而不是一个随机数。
89 |
90 | 设置随机数种子,可以使得图或其中的一部分操作在不同的会话中出现一样的随机数。具体来讲就是如果设置了某一个操作的随机数种子,则在不同的会话中,这个操作生成的随机数序列是完全一样的;如果设置了图级别的随机数种子,则这个图在不同的会话中所有生成的随机数序列都是完全一样的。
91 |
92 | **注意**:如果既设置了图级种子也设置了部分或全部随机数生成操作的种子,那么也会在不同会话中表现一样,只不过最终随机数的种子与默认的不同,其取决于二者。
93 |
94 | 当不设置随机数种子时,会话与会话之间的随机数生成没有关系,如下代码打印出`a`在不同会话中的结果不同:
95 |
96 | ```python
97 | with tf.Graph().as_default():
98 | a = tf.random_uniform([])
99 |
100 | print("Session 1")
101 | with tf.Session() as sess1:
102 | print(sess1.run(a))
103 |
104 | print("Session 2")
105 | with tf.Session() as sess2:
106 | print(sess2.run(a))
107 | ```
108 |
109 | 当设置了随机数种子时,两个会话中生成的随机数序列是完全一样的,如下:
110 |
111 | ```python
112 | with tf.Graph().as_default():
113 | a = tf.random_uniform([], seed=1)
114 |
115 | with tf.Session() as sess1:
116 | res1 = sess1.run(a)
117 |
118 | with tf.Session() as sess2:
119 | res2 = sess2.run(a)
120 |
121 | print(res1 == res2) # >>> True
122 | ```
123 |
124 | 使用图级随机数种子,如下:
125 |
126 | ```python
127 | with tf.Graph().as_default():
128 | tf.set_random_seed(1)
129 | a = tf.random_uniform([])
130 | b = tf.random_normal([])
131 |
132 | with tf.Session() as sess1:
133 | res1_a, res1_b = sess1.run([a, b])
134 |
135 | with tf.Session() as sess2:
136 | res2_a, res2_b = sess2.run([a, b])
137 |
138 | print(res1_a == res2_a) # >>> True
139 | print(res1_b == res2_b) # >>> True
140 | ```
141 |
142 | ### 1.5 特殊常量
143 |
144 | ```python
145 | # 生成指定shape的全0张量
146 | tf.zeros(shape, dtype=tf.float32, name=None)
147 | # 生成与输入的tensor相同shape的全0张量
148 | tf.zeros_like(tensor, dtype=None, name=None,optimize=True)
149 | # 生成指定shape的全1张量
150 | tf.ones(shape, dtype=tf.float32, name=None)
151 | # 生成与输入的tensor相同shap的全1张量
152 | tf.ones_like(tensor, dtype=None, name=None, optimize=True)
153 | # 生成一个使用value填充的shape是dims的张量
154 | tf.fill(dims, value, name=None)
155 | ```
156 |
157 | **小练习**:
158 |
159 | > 1. 使用`tf.constant`生成常量时,能够使用其他常量张量初始化吗?为什么?
160 | > 2. 总结各种常量类型的特点与用法,掌握不同种常量的使用场景。
161 |
162 | ## 2. 变量
163 |
164 | 变量用于存取张量,在Tensorflow中主要使用类`tf.Variable()`来实例化一个变量对象,作用类似于Python中的变量。
165 |
166 | ```python
167 | tf.Variable(
168 | initial_value=None,
169 | trainable=True,
170 | collections=None,
171 | validate_shape=True,
172 | caching_device=None,
173 | name=None,
174 | variable_def=None,
175 | dtype=None,
176 | expected_shape=None,
177 | import_scope=None)
178 | ```
179 |
180 | `initial_value`是必填参数,即变量的初始值。可以使用Python中的`list`、`tuple`、Numpy中的`ndarray`、`Tensor`对象或者其他变量进行初始化。
181 |
182 | ```python
183 | # 使用list初始化
184 | var1 = tf.Variable([1, 2, 3])
185 |
186 | # 使用ndarray初始化
187 | var2 = tf.Variable(np.array([1, 2, 3]))
188 |
189 | # 使用Tensor初始化
190 | var3 = tf.Variable(tf.constant([1, 2, 3]))
191 |
192 | # 使用服从正态分布的随机数Tensor初始化
193 | var4 = tf.Variable(tf.random_normal([3, ]))
194 |
195 | # 使用变量var1初始化
196 | var5 = tf.Variable(var1)
197 | ```
198 |
199 | 这里需要注意的是:使用`tf.Variable()`得到的对象不是Tensor对象,而是承载了`Tensor`对象的`Variable`对象。`Tensor`对象就行是一个“流动对象”,可以存在于各种操作中,包括存在于`Variable`中。所以这也涉及到了如何给变量对象赋值、取值问题。
200 |
201 | #### 使用`tf.get_variable()`创建变量
202 |
203 | 除了使用`tf.Variable()`类实例化一个变量对象以外,还有一种常用的方法来产生一个变量对象:`tf.get_variable()`这是一个生成或获取变量对象的函数。需要注意的是,使用`tf.get_variable()`方法生成一个变量时,其**name不能与已有的name重名**。
204 |
205 | ```python
206 | # 生成一个shape为[3, ]的变量,变量的初值是随机的。
207 | tf.get_variable(name='get_var', shape=[3, ])
208 | #
209 | ```
210 |
211 | 一般的,`tf.get_variable()`与`variable_scope`配合使用会非常方便,在之后的内容中会详细介绍其用法,这里,我们首先知道其可以创建变量对象即可。
212 |
213 | ### 2.1变量初始化
214 |
215 | 变量作为操作的一种,是可以参与图的构建与运行的,但变量在会话中运行时必须在之前进行初始化操作。通常的变量初始化在会话中较早初始化,这可以使之后变量可以正常运行。变量的初始化也是一个操作。直接初始化变量的方法有很多种,例如:
216 |
217 | 1. 使用变量的属性`initializer`进行初始化:
218 |
219 | ```python
220 | var = tf.Variable([1, 2, 3])
221 |
222 | with tf.Session() as sess:
223 | # 如果没有初始化操作,则抛出异常
224 | sess.run(var.initializer)
225 | print(sess.run(var)) # >>> [1, 2, 3]
226 | ```
227 |
228 | 1. 单独初始化每一个变量较为繁琐,可以使用`tf.variables_initializer()`初始化一批变量。
229 |
230 | ```python
231 | var1 = tf.Variable([1, 2, 3])
232 | var2 = tf.Variable([1, 2, 3])
233 |
234 | with tf.Session() as sess:
235 | sess.run(tf.variables_initializer([var1, var2]))
236 | ```
237 |
238 | 1. 上述方法仍然较为繁琐,一般的,所有的变量均需要初始化,这时候就不再需要特别申明。直接使用`tf.global_variables_initialize()`与`tf.local_variables_initializer()`即可初始化全部变量。
239 |
240 | ```python
241 | var1 = tf.Variable(tf.constant([1, 2, 3], dtype=tf.float32))
242 | var2 = tf.Variable(tf.constant([1, 2, 3], dtype=tf.float32))
243 |
244 | with tf.Session() as sess:
245 | sess.run(tf.global_variables_initialize())
246 | # 此处并不存在局部变量,所以不需要`tf.local_variables_initializer()`初始化也可以
247 | sess.run(tf.local_variables_initializer())
248 | ```
249 |
250 | 一般的,我们主动创建的变量均添加进入了一个列表`tf.GraphKeys.GLOBAL_VARIABLES`当中,可以通过`tf.global_variables`获取到所有主动创建的变量。`tf.global_variables_initialize`则是根据`tf.global_variables`中的变量而得到的。除了这些变量之外,有时候某些操作也会附带创建一些变量,但并非我们主动创建的,这其中的一部分变量就添加到了`tf.GraphKeys.LOCAL_VARIABLES`列表当中,可以通过`tf.local_variables`获得全部局部变量,`tf.local_variables_initializer`也是通过此获得全部局部变量的。
251 |
252 | 通常我们使用第三种方法初始化变量。
253 |
254 | 特殊的,在不初始化变量的情况下,也可以使用`tf.Variable.initialized_value()`方法获得其中存储的张量并参与运算,但我们在运行图时,依然需要初始化变量,否则使用到变量的地方依然会出错。
255 |
256 | 直接获取变量中的张量:
257 |
258 | ```python
259 | var1 = tf.Variable([1, 2, 3])
260 | tensor1 = var1.initialized_value()
261 | ```
262 |
263 | ### 2.2 变量赋值
264 |
265 | 变量赋值包含两种情况,第一种情况是定义时进行赋值,第二种是在图运行时修改变量的值:
266 |
267 | ```python
268 | # 定义时赋予变量一个值
269 | A = tf.Variable([1, 2, 3])
270 | with tf.Session() as sess:
271 | sess.run(tf.global_variables_initializer())
272 | print(sess.run(A)) # >> [1, 2, 3]
273 |
274 | # 赋值方法一
275 | sess.run(tf.assign(A, [2, 3, 4]))
276 | print(sess.run(A)) # >> [2, 3, 4]
277 |
278 | # 赋值方法二
279 | sess.run(A.assign([2, 3, 4]))
280 | print(sess.run(A)) # >> [2, 3, 4]
281 | ```
282 |
283 | **注意**:使用`tf.Variable.assign()`或`tf.assign()`进行赋值时,必须要求所赋的值的`shape`与`Variable`对象中张量的`shape`一样、`dtype`一样。
284 |
285 | 除了使用`tf.assign()`以外还可以使用`tf.assign_add()`、`tf.assign_sub()`进行增量赋值。
286 |
287 | ```python
288 | A = tf.Variable(tf.constant([1, 2, 3]))
289 | # 将ref指代的Tensor加上value
290 | # tf.assign_add(ref, value, use_locking=None, name=None)
291 | # 等价于 ref.assign_add(value)
292 | A.assign_add(A, [1, 1, 3]) # >> [2, 3, 6]
293 |
294 | # 将ref指代的Tensor减去value
295 | # tf.assign_sub(ref, value, use_locking=None, name=None)
296 | # 等价于 ref.assign_sub(value)
297 | A.assign_sub(A, [1, 1, 3]) # >> [0, 1, 0]
298 | ```
299 |
300 | ### 2.3 变量操作注意事项
301 |
302 | - 注意事项一:
303 |
304 | 当我们在会话中运行并输出一个初始化并再次复制的变量时,输出是多少?如下:
305 |
306 | ```python
307 | W = tf.Variable(10)
308 | W.assign(100)
309 | with tf.Session() as sess:
310 | sess.run(W.initializer)
311 | sess.run(W)
312 | ```
313 |
314 | 上面的代码将输出`10`而不是`100`,原因是`w`并不依赖于其赋值操作`W.assign(100)`,`W.assign(100)`产生了一个OP,然而在`sess.run()`的时候并没有执行这个OP,所以并没有赋值。需要改为:
315 |
316 | ```python
317 | W = tf.Variable(10)
318 | assign_op = W.assign(100)
319 | with tf.Session() as sess:
320 | sess.run(W.initializer)
321 | sess.run(assign_op)
322 | sess.run(W)
323 | ```
324 |
325 | - 注意事项二:
326 |
327 | 重复运行变量赋值语句会发生什么?
328 |
329 | ```python
330 | var = tf.Variable(1)
331 | assign_op = var.assign(2 * var)
332 |
333 | with tf.Session() as sess:
334 | sess.run(var.initializer)
335 | sess.run(assign_op)
336 | sess.run(var) # > 2
337 |
338 | sess.run(assign_op)
339 | sess.run(var) # > ???
340 | ```
341 |
342 | 这里第二次会输出`4`,因为运行了两次赋值op。第一次运行完成时,`var`被赋值为`2`,第二次运行时被赋值为`4`。
343 |
344 | 那么改为如下情况呢?
345 |
346 | ```python
347 | var = tf.Variable(1)
348 | assign_op = var.assign(2 * var)
349 |
350 | with tf.Session() as sess:
351 | sess.run(var.initializer)
352 | sess.run([assign_op, assign_op])
353 | sess.run(var) # ???
354 | ```
355 |
356 | 这里,会输出`2`。会话`run`一次,图执行一次,而`sess.run([assign_op, assign_op])`仅仅相当于查看了两次执行结果,并不是执行了两次。
357 |
358 | 那么改为如下情况呢?
359 |
360 | ```python
361 | var = tf.Variable(1)
362 | assign_op_1 = var.assign(2 * var)
363 | assign_op_2 = var.assign(3 * var)
364 |
365 | with tf.Session() as sess:
366 | sess.run(var.initializer)
367 | sess.run([assign_op_1, assign_op_2])
368 | sess.run(var) # >> ??
369 | ```
370 |
371 | 这里两次赋值的Op相当于一个图中的两个子图,其执行顺序不分先后,由于两个子图的执行结果会对公共的变量产生影响,当子图A的执行速度快于子图B时,可能是一种结果,反之是另一种结果,所以这样的写法是不安全的写法,执行的结果是不可预知的,所以要避免此种情况出现。但可以通过控制依赖来强制控制两个子图的执行顺序。
372 |
373 | - 注意事项三:
374 |
375 | 在多个图中给一个变量赋值:
376 |
377 | ```python
378 | W = tf.Variable(10)
379 | sess1 = tf.Session()
380 | sess2 = tf.Session()
381 |
382 | sess1.run(W.initializer)
383 | sess2.run(W.initializer)
384 |
385 | print(sess1.run(W.assign_add(10))) # >> 20
386 | print(sess2.run(W.assign_sub(2))) # ???
387 |
388 | sess1.close()
389 | sess2.close()
390 | ```
391 |
392 | 第二个会打印出`8`。因为在两个图中的OP是互不相干的。**每个会话都保留自己的变量副本**,它们分别执行得到结果。
393 |
394 | - 注意事项四:
395 |
396 | 使用一个变量初始化另一个变量时:
397 |
398 | ```python
399 | a = tf.Variable(1)
400 | b = tf.Variable(a)
401 |
402 | with tf.Session() as sess:
403 | sess.run(b.initializer) # 抛出异常
404 | ```
405 |
406 | 出错的原因是`a`没有初始化,`b`就无法初始化。所以使用一个变量初始化另一个变量时,会带来不安全因素。为了确保初始化时不会出错,可以使用如下方法:
407 |
408 | ```python
409 | a = tf.Variable(1)
410 | b = tf.Variable(a.initialized_value())
411 |
412 | with tf.Session() as sess:
413 | sess.run(b.initializer)
414 | ```
415 |
416 | - 注意事项五:
417 |
418 | 变量初始化操作应该置于其他变量操作之前。事实上所有在一次会话执行中的操作与张量,都应该考虑其执行顺序是否会有关联,如有关联,则可能出现不可预知的错误。例如:
419 |
420 | ```python
421 | W = tf.Variable(10)
422 | assign_op = W.assign(100)
423 | with tf.Session() as sess:
424 | sess.run([W.initializer, assign_op])
425 | print(sess.run(W)) # W 可能为10或100
426 | ```
427 |
428 | 正确的写法:
429 |
430 | ```python
431 | W = tf.Variable(10)
432 | assign_op = W.assign(100)
433 | with tf.Session() as sess:
434 | sess.run(W.initializer)
435 | sess.run(assign_op)
436 | print(sess.run(W))
437 | ```
438 |
439 | - 注意事项六:
440 |
441 | 重新初始化变量之后,变量的值变为初始值:
442 |
443 | ```python
444 | W = tf.Variable(10)
445 | with tf.Session() as sess:
446 | sess.run(W.initializer)
447 | sess.run(W.assign(100))
448 | sess.run(W.initializer)
449 | print(sess.run(W)) # >>> 10
450 | ```
451 |
452 | **小练习**:
453 |
454 | > 练习上述注意事项代码。
455 |
456 | ## 3. Tensor、ndarray、原生数据之间的相互转化
457 |
458 | 使用Tensorflow的Python API仅仅是构建图,图的执行由TensorFlow的**内核**(操作在不同设备上的C++实现)来完成。也就是说图的定义以及部分数据的传入是在Python环境中完成的,图的执行是在C++环境中完成的,最后执行图所得到的结果往往也需要返回给Python环境。所以使用TensorFlow就不可避免的需要了解不同环境中数据的对应关系以及相互转化方法。
459 |
460 | **注意**:不同设备上的数据在不同执行环境中频繁的交互会降低性能,通常的我们会尽量避免频繁交互。
461 |
462 | ### 3.1 原生数据、ndarray转化为Tensor
463 |
464 | 一般的,构建常量、变量等图结构时经常会使用Python原生数据与数据结构作为输入,例如构建一个常量:
465 |
466 | ```python
467 | tf.constant([1, 2])
468 | ```
469 |
470 | 这时等价于将Python环境中的数据转化为了TensorFlow中的张量,默认的,整形转化为`DT_INT32`类型,浮点型转化为`DT_FLOAT`(32位)类型,复数转化为`DT_COMPLEX128`类型。
471 |
472 | 类似的,我们也可以将ndarray类型的数据输入到图中,例如:
473 |
474 | ```python
475 | tf.constant(np.array([1, 2], dtype=np.float32))
476 | ```
477 |
478 | 与直接输入Python数据与数据结构不同的是,输入ndarray数据,TensorFlow则会继承其数据类型,上述代码中,得到的Tensor数据类型为`DT_FLOAT`。
479 |
480 | 注意:有些Python原生数据不属于张量,并不能转化为张量,例如:`t = [1, [2, 3]]`。
481 |
482 | ### 3.2 Tensor 转化为原生数据、ndarray
483 |
484 | 当完成图的构建之后,在会话中运行图则可以得到结果,这时候会话返回给Python环境中的结果也是包可能是Python原生数据或ndarray数据。一般的张量执行的结果均返回ndarray数据(这是因为TensorFlow内核使用了ndarray数据结构)。
485 |
486 | ```python
487 | a = tf.constant([1, 2, 3])
488 |
489 | with tf.Session() as sess:
490 | res = sess.run(a)
491 | print(res) # >>> [1 2 3]
492 | print(type(res)) # >>>
493 | ```
494 |
495 | ## 4. 占位符
496 |
497 | 使用TensorFlow定义图,类似于定义了一个算式。但这是不方便的,为了能够在复杂问题中能够流畅构建运算逻辑,我们还需要引入“未知数”来参与构建图,就像代数中的方程中引入未知数一样。在TensorFlow中,我们称之为**占位符(placehoders)**。
498 |
499 | 引入占位符在TensorFlow中是很重要的,它可以把图的构建与数据的输入关系解耦,这意味着构建一个图只需要知道数据的格式即可,而不需要将庞大的数据输入图。仍然以方程为例$z=x*2+y$,当我们在需要用到这个方程时,再给$x,y$赋值,就能立马得到结果,方程$z=x*2+y$就相当于图,$x,y$就相当于占位符。在TensorFlow中使用`tf.placeholder`构建占位符。占位符也是一个节点。
500 |
501 | ```python
502 | tf.placeholder(dtype, shape=None, name=None)
503 | ```
504 |
505 | 例如,我们需要将上述方程使用带占位符的图描述:
506 |
507 | ```python
508 | x = tf.placeholder(dtype=tf.float32, shape=[])
509 | y = tf.placeholder(dtype=tf.float32, shape=[])
510 |
511 | z = tf.multiply(x, 2) + y
512 | ```
513 |
514 | **注意**:占位符必须包含数据类型,但可以没有`shape`,即可以将`shape`设置为`None`或者将`shape`中的某一些维度设置为`None`。一般的,如果`shape`可知,则尽量输入到占位符中,可以避免出现很多不必要的错误。
515 |
516 | ### 4.1 feed_dict
517 |
518 | 图构建好之后,运行图时,需要使用张量替代占位符,否则这个图无法运行,如下:
519 |
520 | ```python
521 | x = tf.placeholder(dtype=tf.float32, shape=[])
522 | y = tf.placeholder(dtype=tf.float32, shape=[])
523 |
524 | z = tf.multiply(x, 2) + y
525 |
526 | with tf.Session() as sess:
527 | sess.run(z, feed_dict={x: 5, y: 10}) # >>> 20.0
528 | ```
529 |
530 | 可以看到使用占位符,需要输入占位数据的类型即可,即不需要填入具体数据。占位符可以不设确定的`shape`意味着可以使用不同`shape`但运算规则一致的数据。例如:
531 |
532 | ```python
533 | x = tf.placeholder(dtype=tf.float32)
534 | y = tf.placeholder(dtype=tf.float32)
535 |
536 | z = tf.multiply(x, 2) + y
537 |
538 | with tf.Session() as sess:
539 | sess.run(z, feed_dict={x: 5, y: 10}) # >>> 20.0
540 | sess.run(z, feed_dict={x: [5, 4], y: [10, 18]}) # >>> [20., 26.]
541 | ```
542 |
543 | ### 4.2 feed_dict的更多用法
544 |
545 | 除了`tf.placeholder`可以并且必须使用张量替代以外,很多张量均可以使用`feed_dict`替代,例如:
546 |
547 | ```python
548 | a = multiply(1, 2)
549 |
550 | with tf.Session() as sess:
551 | sess.run(a, feed_dict={a: 10}) # >> 10
552 | ```
553 |
554 | 为了保证张量的替代是合法的,可以使用`tf.Graph.is_feedable(tensor)`检查`tensor`是否可以替代:
555 |
556 | ```python
557 | a = multiply(1, 2)
558 | tf.get_default_graph().is_feedable(a) # True
559 | ```
560 |
561 | **注意**:变量、占位符是两类完全不同功能的节点。变量在图的运行中负责保存当前时刻一个张量的具体数值,在运行的不同阶段、时刻可能是不同的,职责是存储运行时的临时张量;而占位符是在图运行之前的充当某一个张量的替身,在运行时必须使用一个张量替代,职责是建构时代替具体的张量值。
562 |
563 | **小练习**:
564 |
565 | > 回顾上述内容并练习编写上述代码。
566 |
567 | ## 作业
568 |
569 | 利用所学知识,完成以下任务:
570 |
571 | 1. 构建二元线性回归模型,其中模型中的参数使用`tf.Variable()`构建,模型的样本输入使用`tf.placeholder`代替。写出模型结构。
572 | 2. 使用`tf.placeholder`代替上述样本中的标记,写出对于一个样本的代价。
--------------------------------------------------------------------------------
/ch05/ch5-name-and-scope.md:
--------------------------------------------------------------------------------
1 | TensorFlow中所有的`Tensor`、`Operation`均有`name`属性,是其唯一标识符。在Python中,一个变量可以绑定一个对象,同样的也可以绑定一个`Tensor`或`Operation`对象,但这个变量并不是`Tensor`或`Operation`的唯一标识符。
2 |
3 | 例如,下面的代码中,Python变量`a`首先指向`tf.constant([1, 2, 3])`,之后又指向了`tf.Variable([4, 5, 6])`,那么最终图中仍然包含两块内容,但是使用`a`只能获取到后定义的`tf.Variable([4, 5, 6])`,:
4 |
5 | ```python
6 | a = tf.constant([1, 2, 3])
7 | a = tf.Variable([4, 5, 6])
8 | ```
9 |
10 | 这时候就只能使用TensorFlow中的`name`解决问题。除此以外,使用`name`可以使得TensorFlow使用某一个编程语言编写的图在其他语言中可以方便操作,实现跨语言环境的兼容。使用`name`也有助于在TensorBoard中可视化。在之前的操作中,我们一般不指定`Tensor`、`Operation`的`name`属性,但这并不代表其没有`name`属性,而是使用了默认的命名。
11 |
12 | 除此以外,Tensorflow也有作用域(scope),用来管理`Tensor`、`Operation`的`name`以及完成一些可复用的操作。Tensorflow的作用域分为两种,一种是`variable_scope`,另一种是`name_scope`。
13 |
14 | ## 1. name
15 |
16 | `Tensor`与`Operation`均有`name`属性,但我们只能给`Operation`进行主动命名,`Tensor`的`name`由`Operation`根据自己的`name`与输出数量进行命名(所有的`Tensor`均由`Operation`产生)。
17 |
18 | 例如,我们定义一个常量,并赋予其`name`:
19 |
20 | ```python
21 | a = tf.constat([1, 2, 3], name='const')
22 | ```
23 |
24 | 这里我们给常量`Op`定义了一个`name`为`const`。`a`是常量`Op`的返回值(输出),是一个张量`Tensor`对象,所以`a`也有自己的`name`,为`const:0`。
25 |
26 | 可以看到:`Operation`的`name`是我们进行命名的,其输出的张量在其后增加了冒号与索引,是TensorFlow根据`Operation`的`name`进行命名的。
27 |
28 | ### 1.1 Op的name命名规范
29 |
30 | 首先`Tensor`对象的`name`我们并不能直接进行操作,我们只能给`Op`设置`name`。`Op`的命令规范规范是:**由数字、字母、下划线组成,不能以下划线开头,且不区分大小写**。
31 |
32 | **正确**的命名方式如下:
33 |
34 | ```python
35 | a1 = tf.constant([1, 2, 3], name='const')
36 | a2 = tf.Variable([1, 2, 3], name='123')
37 | a3 = tf.add(1, 2, name='const_')
38 | ```
39 |
40 | **错误**的命名方式如下:
41 |
42 | ```python
43 | a1 = tf.constant([1, 2, 3], name='_const')
44 | a2 = tf.Variable([1, 2, 3], name='/123')
45 | a3 = tf.add(1, 2, name='const:0')
46 | ```
47 |
48 | 每个`Op`都有`name`属性,可以通过属性查看`name`值,例如:
49 |
50 | ```python
51 | # 返回一个什么都不做的Op
52 | op = tf.no_op(name='hello')
53 |
54 | print(op.name) # hello
55 | ```
56 |
57 | 这里我们列举了一个空`Op`,而没有使用常用的诸如`tf.add`这样的`Op`,是因为默认的大部分`Op`都返回对应的张量,而不是`Op`对象,但`tf.no_op`函数返回的是`Op`对象,是一个特例。
58 |
59 | ### 1.2 Tensor的name构成
60 |
61 | 大部分的`Op`会有返回值,其返回值一般是一个或多个`Tensor`。`Tensor`的`name`并不来源于我们设置,只能来源于生成它的`Op`,所以`Tensor`的`name`是由`Op`的`name`所决定的。
62 |
63 | `Tensor`的`name`构成很简单,即在对应的`Op`的`name`之后加上输出索引。即由以下三部分构成:
64 |
65 | 1. 生成此`Tensor`的`op`的`name`;
66 | 2. 冒号;
67 | 3. `op`输出内容的索引,索引默认从`0`开始。
68 |
69 | 例如:
70 |
71 | ```python
72 | a = tf.constant([1, 2, 3], name='const')
73 |
74 | print(a.name) # const:0
75 | ```
76 |
77 | 这里,我们设置了常量`Op`的`name`为`const`,这个`Op`会返回一个`Tensor`,所以返回的`Tensor`的`name`就是在其后加上冒号与索引。由于只有一个输出,所以这个输出的索引就是`0`。
78 |
79 | 对于两个或多个的输出,其索引依次增加:如下:
80 |
81 | ```python
82 | key, value = tf.ReaderBase.read(..., name='read')
83 |
84 | print(key.name) # read:0
85 | print(value.name) # read:1
86 | ```
87 |
88 | ### 1.3 Op与Tensor的默认name
89 |
90 | 当我们不去设置`Op`的`name`时,TensorFlow也会默认设置一个`name`,这也正是`name`为可选参数的原因。默认`name`往往与`Op`的类型相同(默认的`name`并无严格的命名规律)。
91 |
92 | 例如:
93 |
94 | ```python
95 | a = tf.add(1, 2)
96 | # op name为 `Add`
97 | # Tensor name 为 `Add:0`
98 |
99 | b = tf.constant(1)
100 | # op name 为 `Const`
101 | # Tensor name 为 `Const:0`
102 |
103 | c = tf.divide(tf.constant(1), tf.constant(2))
104 | # op name 为 `truediv`
105 | # Tensor name 为 `truediv:0`
106 | ```
107 |
108 | **注意**:还有一些特殊的`Op`,我们没法指定其`name`,只能使用默认的`name`,例如:
109 |
110 | ```python
111 | init = tf.global_variables_initializer()
112 | print(init.name) # init
113 | ```
114 |
115 | ### 1.4 重复name的处理方式
116 |
117 | 虽然`name`作为唯一性的标识符,但TensorFlow并不会强制要求我们必须设置完全不同的`name`,这并非说明`name`可以重复,而是TensorFlow通过一些方法避免了`name`重复。
118 |
119 | 当出现了两个`Op`设置相同的`name`时,TensorFlow会自动给后面的`op`的`name`加一个后缀。如下:
120 |
121 | ```python
122 | a1 = tf.add(1, 2, name='my_add')
123 | a2 = tf.add(3, 4, name='my_add')
124 |
125 | print(a1.name) # my_add:0
126 | print(a2.name) # my_add_1:0
127 | ```
128 |
129 | 后缀由下划线与索引组成(注意与`Tensor`的`name`属性后的缀冒号索引区分)。从重复的第二个`name`开始加后缀,后缀的索引从`1`开始。
130 |
131 | 当我们不指定`name`时,使用默认的`name`也是相同的处理方式:
132 |
133 | ```python
134 | a1 = tf.add(1, 2)
135 | a2 = tf.add(3, 4)
136 |
137 | print(a1.name) # Add:0
138 | print(a2.name) # Add_1:0
139 | ```
140 |
141 | 不同操作之间是有相同的`name`也是如此:
142 |
143 | ```python
144 | a1 = tf.add(1, 2, name='my_name')
145 | a2 = tf.subtract(1, 2, name='my_name')
146 |
147 | print(a1.name) # >>> my_name:0
148 | print(a2.name) # >>> my_name_1:0
149 | ```
150 |
151 | **注意**:设置`name`时,如果重复设置了一样的`name`,并不会抛出异常,也不会有任何提示,TensorFlow会自动添加后缀。为了避免出现意外情况,通常的`name`设置必须不重复。
152 |
153 | ### 1.5 不同图中相同操作name
154 |
155 | 当我们构建了两个或多个图的时候,如果这些图中有相同的操作或者相同的`name`时,并不会互相影响。如下:
156 |
157 | ```python
158 | g1 = tf.Graph()
159 | with g1.as_default():
160 | a1 = tf.add(1, 2, name='add')
161 | print(a1.name) # add:0
162 |
163 | g2 = tf.Graph()
164 | with g2.as_default():
165 | a2 = tf.add(1, 2, name='add')
166 | print(a2.name) # add:0
167 | ```
168 |
169 | 可以看到两个图中的`name`互不影响。并没有关系。
170 |
171 | **小练习**:
172 |
173 | > 以下操作均为一个图中的`op`,请写出以下操作对应中的`Op`与对应生成的`Tensor`的`name`:
174 | >
175 | > - `tf.constant([1, 2])`
176 | > - `tf.add([1, 2], [3, 4], name='op_1')`
177 | > - `tf.add([2, 3], [4, 5], name='op_1')`
178 | > - `tf.mod([1, 3], [2, 4], name='op_1')`
179 | > - `tf.slice([1, 2], [0], [1], name='123')`
180 |
181 | ## 2. 通过name获取Op与Tensor
182 |
183 | 上文,我们介绍了`name`可以看做是`Op`与`Tensor`的唯一标识符。所以可以通过一些方法利用`name`获取到`Op`与`Tensor`。
184 |
185 | 例如,一个计算过程如下:
186 |
187 | ```python
188 | g1 = tf.Graph()
189 | with g1.as_default():
190 | a = tf.add(3, 5)
191 | b = tf.multiply(a, 10)
192 |
193 | with tf.Session(graph=g1) as sess:
194 | sess.run(b) # 80
195 | ```
196 |
197 | 上述图,我们使用了Python变量`b`获取对应的操作,我们也可以使用如下方式获取,两种方式结果一样:
198 |
199 | ```python
200 | g1 = tf.Graph()
201 | with g1.as_default():
202 | tf.add(3, 5, name='add')
203 | tf.multiply(g1.get_tensor_by_name('add:0'), 10, name='mul')
204 |
205 | with tf.Session(graph=g1) as sess:
206 | sess.run(g1.get_tensor_by_name('mul:0')) # 80
207 | ```
208 |
209 | 这里使用了`tf.Graph.get_tensor_by_name`方法。可以根据`name`获取`Tensor`。其返回值是一个`Tensor`对象。这里要注意`Tensor`的`name`必须写完整,请勿将`Op`的`name`当做是`Tensor`的`name`。
210 |
211 | 同样的,利用`name`也可以获取到相应的`Op`,这里需要使用`tf.Graph.get_operation_by_name`方法。上述例子中,我们在会话中运行的是乘法操作的返回值`b`。运行`b`的时候,与其相关的依赖,包括乘法`Op`也运行了,当我们不需要返回值时,我们在会话中可以直接运行`Op`,而无需运行`Tensor`。
212 |
213 | 例如:
214 |
215 | ```python
216 | g1 = tf.Graph()
217 | with g1.as_default():
218 | tf.add(3, 5, name='add')
219 | tf.multiply(g1.get_tensor_by_name('add:0'), 10, name='mul')
220 |
221 | with tf.Session(graph=g1) as sess:
222 | sess.run(g1.get_operation_by_name('mul')) # None
223 | ```
224 |
225 | 在会话中,`fetch`一个`Tensor`,会返回一个`Tensor`,`fetch`一个`Op`,返回`None`。
226 |
227 | **小练习**:
228 |
229 | > 请自己尝试实现上述代码。
230 |
231 | ## 3. name_scope
232 |
233 | 随着构建的图越来越复杂,直接使用`name`对图中的节点命名会出现一些问题。比如功能近似的节点`name`可能命名重复,也难以通过`name`对不同功能的节点加以区分,这时候如果可视化图会发现将全部节点展示出来是杂乱无章的。为了解决这些问题,可以使用`name_scope`。
234 |
235 | `name_scope`可以为其作用域中的节点的`name`添加一个或多个前缀,并使用这些前缀作为划分内部与外部`op`范围的标记。同时在TensorBoard可视化时可以作为一个整体出现(也可以展开)。并且`name_scope`可以嵌套使用,代表不同层级的功能区域的划分。
236 |
237 | `name_scope`使用`tf.name_scope()`创建,返回一个上下文管理器。`name_scope`的参数`name`可以是字母、数字、下划线,不能以下划线开头。类似于`Op`的`name`的命名方式。
238 |
239 | `tf.name_scope()`的详情如下:
240 |
241 | ```python
242 | tf.name_scope(
243 | name, # 传递给Op name的前缀部分
244 | default_name=None, # 默认name
245 | values=None) # 检测values中的tensor是否与下文中的Op在一个图中
246 | ```
247 |
248 | **注意**:`values`参数可以不填。当存在多个图时,可能会出现在当前图中使用了在别的图中的`Tensor`的错误写法,此时如果不在`Session`中运行图,并不会抛出异常,而填写到了`values`参数的中的`Tensor`都会检测其所在图是否为当前图,提高安全性。
249 |
250 | 使用`tf.name_scope()`的例子:
251 |
252 | ```python
253 | a = tf.constant(1, name='const')
254 | print(a.name) # >> const:0
255 |
256 | with tf.name_scope('scope_name') as name:
257 | print(name) # >> scope_name/
258 | b = tf.constant(1, name='const')
259 | print(b.name) # >> scope_name/const:0
260 | ```
261 |
262 | 在一个`name_scope`的作用域中,可以填写`name`相同的`Op`,但TensorFlow会自动加后缀,如下:
263 |
264 | ```python
265 | with tf.name_scope('scope_name') as name:
266 | a1 = tf.constant(1, name='const')
267 | print(b.name) # scope_name/const:0
268 | a2 = tf.constant(1, name='const')
269 | print(c.name) # scope_name/const_1:0
270 | ```
271 |
272 | #### 多个name_scope
273 |
274 | 我们可以指定任意多个`name_scope`,并且可以填写相同`name`的两个或多个`name_scope`,但TensorFlow会自动给`name_scope`的`name`加上后缀:
275 |
276 | 如下:
277 |
278 | ```python
279 | with tf.name_scope('my_name') as name1:
280 | print(name1) # >> my_name/
281 |
282 | with tf.name_scope('my_name') as name2:
283 | print(name2) #>> my_name_1/
284 | ```
285 |
286 | ### 3.1 多级name_scope
287 |
288 | `name_scope`可以嵌套,嵌套之后的`name`包含上级`name_scope`的`name`。通过嵌套,可以实现多样的命名,如下:
289 |
290 | ```python
291 | with tf.name_scope('name1'):
292 | with tf.name_scope('name2') as name2:
293 | print(name2) # >> name1/name2/
294 | ```
295 |
296 | 不同级的`name_scope`可以填入相同的`name`(不同级的`name_scope`不存在同名),如下:
297 |
298 | ```python
299 | with tf.name_scope('name1') as name1:
300 | print(name1) # >> name1/
301 | with tf.name_scope('name1') as name2:
302 | print(name2) # >> name1/name1/
303 | ```
304 |
305 | 在多级`name_scope`中,`Op`的`name`会被累加各级前缀,这个前缀取决于所在的`name_scope`的层级。不同级中的`name`因为其前缀不同,所以不可能重名,如下:
306 |
307 | ```python
308 | with tf.name_scope('name1'):
309 | a = tf.constant(1, name='const')
310 | print(a.name) # >> name1/const:0
311 | with tf.name_scope('name2'):
312 | b = tf.constant(1, name='const')
313 | print(b.name) # >> name1/name2/const:0
314 | ```
315 |
316 | ### 3.2 name_scope的作用范围
317 |
318 | 使用`name_scope`可以给`Op`的`name`加前缀,但不包括`tf.get_variable()`创建的变量,如下所示:
319 |
320 | ```python
321 | with tf.name_scope('name'):
322 | var = tf.Variable([1, 2], name='var')
323 | print(var.name) # >> name/var:0
324 | var2 = tf.get_variable(name='var2', shape=[2, ])
325 | print(var2.name) # >> var2:0
326 | ```
327 |
328 | 这是因为`tf.get_variable`是一种特殊的操作,其只能与`variable_scope`配合完成相应功能。
329 |
330 | ### 3.3 注意事项
331 |
332 | 1. 从外部传入的`Tensor`,并不会在`name_scope`中加上前缀。例如:
333 |
334 | ```python
335 | a = tf.constant(1, name='const')
336 | with tf.name_scope('my_name', values=[a]):
337 | print(a.name) # >> const:0
338 | ```
339 |
340 | 2. `Op`与`name_scope`的`name`中可以使用`/`,但`/`并不是`name`的构成,而是区分命名空间的符号,不推荐直接使用`/`。
341 |
342 | 3. `name_scope`的`default_name`参数可以在函数中使用。`name_scope`返回的`str`类型的`scope`可以作为`name`传给函数中返回`Op`的`name`,这样做的好处是返回的`Tensor`的`name`反映了其所在的模块。例如:
343 |
344 | ```python
345 | def my_op(a, b, c, name=None):
346 | with tf.name_scope(name, "MyOp", [a, b, c]) as scope:
347 | a = tf.convert_to_tensor(a, name="a")
348 | b = tf.convert_to_tensor(b, name="b")
349 | c = tf.convert_to_tensor(c, name="c")
350 | # Define some computation that uses `a`, `b`, and `c`.
351 | return foo_op(..., name=scope)
352 | ```
353 |
354 | **小练习**:
355 |
356 | > 以下说法正确的是:
357 | >
358 | > - `name_scope`可以给所有在其作用域中创建的`Op`的`name`添加前缀。
359 | > - 在多级`name_scope`中的不同层级作用域下创建的`Op`(除去`tf.get_variable`以外),不存在`name`重名。
360 | > - `name_scope`可以通过划分操作范围来组织图结构,并能服务于得可视化。
361 |
362 | ## 4. variable_scope
363 |
364 | `variable_scope`主要用于管理变量作用域以及与变量相关的操作,同时`variable_scope`也可以像`name_scope`一样用来给不同操作区域划分范围(添加`name`前缀)。`variable_scope`功能也要更丰富,最重要的是可以与`tf.get_variable()`等配合使用完成对变量的重复使用。
365 |
366 | `variable_scope`使用`tf.variable_scope()`创建,返回一个上下文管理器。
367 |
368 | `tf.variable_scope`的详情如下:
369 |
370 | ```python
371 | variable_scope(name_or_scope, # 可以是name或者别的variable_scope
372 | default_name=None,
373 | values=None,
374 | initializer=None, # 作用域中的变量默认初始化方法
375 | regularizer=None, # 作用域中的变量默认正则化方法
376 | caching_device=None, # 默认缓存变量的device
377 | partitioner=None, # 用于应用在被优化之后的投影变量操作
378 | custom_getter=None, # 默认的自定义的变量getter方法
379 | reuse=None, # 变量重用状态
380 | dtype=None, # 默认的创建变量的类型
381 | use_resource=None): # 是否使用ResourceVariables代替默认的Variables
382 | ```
383 |
384 | ### 4.1 给Op的name加上name_scope
385 |
386 | `variable_scope`包含了`name_scope`的全部功能,即在`variable_scope`下也可以给`Op`与`Tensor`加上`name_scope`:
387 |
388 | ```python
389 | with tf.variable_scope('abc') as scope:
390 | a = tf.constant(1, name='const')
391 | print(a.name) # >> abc/const:0
392 | ```
393 |
394 | **注意**:默认的`variable_scope`的`name`等于其对应的`name_scope`的`name`,但并不总是这样。我们可以通过如下方法查看其`variable_scope`的`scope_name`与`name_scope`的`scope_name`:
395 |
396 | ```python
397 | g = tf.Graph()
398 | with g.as_default():
399 | with tf.variable_scope('abc') as scope:
400 | # 输出variable_scope的`name`
401 | print(scope.name) # >>> abc
402 |
403 | n_scope = g.get_name_scope()
404 | # 输出name_scope的`name`
405 | print(n_scope) # >>> abc
406 | ```
407 |
408 | ### 4.2 同名variable_scope
409 |
410 | 创建两个或多个`variable_scope`时可以填入相同的`name`,此时相当于创建了一个`variable_scope`与两个或多个`name_scope`。
411 |
412 | ```python
413 | g = tf.Graph()
414 | with g.as_default():
415 | with tf.variable_scope('abc') as scope:
416 | print(scope.name) # >> abc
417 | n_scope = g.get_name_scope()
418 | print(n_scope) # >> abc
419 |
420 | with tf.variable_scope('abc') as scope:
421 | print(scope.name) # >> abc
422 | n_scope = g.get_name_scope()
423 | print(n_scope) # >> abc_1
424 | ```
425 |
426 | ### 4.3 与get_variable()的用法
427 |
428 | `variable_scope`的最佳搭档是`tf.get_variable()`函数。一般的,我们会在`variable_scope`中使用`tf.get_variable()`创建与获取模型的变量,并且`variable_scope`为此提供了更多便利。
429 |
430 | #### 4.3.1 独立使用get_variable()
431 |
432 | 与使用`tf.Variable()`不同,独立的使用(不在变量作用域中时)`tf.get_variable()`创建变量时不需要提供初始化的值,但必须提供`name`、`shape`、`dtype`,这是确定一个变量的基本要素。使用`tf.get_variable`创建变量的方法如下:
433 |
434 | ```python
435 | tf.get_variable('abc', dtype=tf.float32, shape=[])
436 | ```
437 |
438 | **说明**:没有初始化的值,并不意味着没有值,事实上它的值是随机的。使用`tf.Variable()`创建的变量,一般不需要提供`shape`、`dtype`,这是因为可以从初始化的值中推断出来,也不需要`name`,是因为默认的TensorFlow提供了自动生成`name`的方法。
439 |
440 | `tf.get_variable()`还有一个特点是必须提供独一无二的`name`在当前变量作用域下,如果提供了重名的`name`才会抛出异常,如下:
441 |
442 | ```python
443 | a = tf.get_variable('abcd', shape=[1])
444 | b = tf.get_variable('abcd', shape=[1]) # ValueError
445 | ```
446 |
447 | #### 4.3.2 在变量作用域中使用get_variable()
448 |
449 | `variable_scope`对象包含一个`reuse`属性,默认的值为`None`,在这种情况下,代表`variable_scope`不是可重用的,此时,在`variable_scope`中的`tf.get_variable()`用法与上述独立使用`tf.get_variable()`用法完全一致。`tf.get_variable()`在`variable_scope`中创建的变量会被添加上`variable_scope`的`scope_name`前缀。当`variable_scope`的`reuse`属性值为`True`时,代表此`variable_scope`是可重用的,此时在`variable_scope`中的`tf.get_variable()`用法变成了利用`name`获取已存在的变量,而无法创建变量。也就是说`tf.get_variable()`的用法是随着`reuse`的状态而改变的,例如:
450 |
451 | ```python
452 | with tf.variable_scope('scope', reuse=None) as scope:
453 | # 此时reuse=None,可以用来创建变量
454 | tf.get_variable('var', dtype=tf.float32, shape=[])
455 | # 改修reuse=True
456 | scope.reuse_variables()
457 | # 此时reuse=True,可以用来获得已有变量
458 | var = tf.get_variable('var')
459 | ```
460 |
461 | 上述的写法也可以写成下面类似形式:
462 |
463 | ```python
464 | with tf.variable_scope('scope', reuse=None) as scope:
465 | tf.get_variable('var', dtype=tf.float32, shape=[])
466 |
467 | with tf.variable_scope(scope, reuse=True) as scope:
468 | var = tf.get_variable('var')
469 | ```
470 |
471 | 可以看到下面的`scope`直接使用上面生成的`scope`而生成,也就是说`variable_scope`只要是`name`一样或者使用同一个`scope`生成,那么这些`variable_scope`都是同一个`variable_scope`。
472 |
473 | **注意**:以上两种写法从`variable_scope`的角度看是等价的,但每创建一个`variable_scope`都创建了一个`name_scope`,所以上面的写法只包含一个`name_scope`,而下面的写法包含两个`name_scope`。这也是上文提到的`variable_scope`的`scope_name`与其包含的`name_scope`的`scope_name`不完全一样的原因。
474 |
475 | 为了便捷,`reuse`属性也可以设置为`tf.AUTO_REUSE`,这样`variable_scope`会根据情况自动判断变量的生成与获取,如下:
476 |
477 | ```python
478 | with tf.Graph().as_default():
479 | with tf.variable_scope('scope', reuse=None):
480 | tf.get_variable('my_var_a', shape=[], dtype=tf.float32)
481 |
482 | with tf.variable_scope('scope', reuse=tf.AUTO_REUSE):
483 | a = tf.get_variable('my_var_a') # 获取变量
484 | b = tf.get_variable('my_var_b', shape=[], dtype=tf.float32) # 生成一个变量
485 |
486 | with tf.Session() as sess:
487 | sess.run(tf.global_variables_initializer())
488 | print(sess.run([a, b]))
489 | ```
490 |
491 | `variable_scope`与`tf.get_variable`配合使用这样的写法在较大型模型中是非常有用的,可以使得模型变量的复用变得容易。
492 |
493 | **小结**:
494 |
495 | - 在变量作用域中,如其属性`reuse=None`时,`tf.get_variable`不能获得变量;
496 | - 在变量作用域中,如其属性`reuse=True`时,`tf.get_variable`不能创建变量;
497 | - 在变量作用域中,`scope.reuse_variables()`可以改变下文的`reuse`属性值为`True`;
498 | - 同名的多个变量作用域所处的上下文中的名字作用域不同。
499 |
500 | **小练习**:
501 |
502 | > 尝试实验验证上文“注意”中提到一个`variable_scope`与两个同名`variable_scope`中`name_scope`的情况。
503 |
504 | ### 4.4 多级变量作用域
505 |
506 | 变量作用域是可以嵌套使用的,这时候其`name`前缀也会嵌套。
507 |
508 | 如下:
509 |
510 | ```python
511 | with tf.variable_scope('first_scope') as first_scope:
512 | print(first_scope.name) # >> first_scope
513 | with tf.variable_scope('second_scope') as second_scope:
514 | print(second_scope.name) # >> first_scope/second_scope
515 | print(tf.get_variable('var', shape=[1, 2]).name)
516 | # >> first_scope/second_scope/var:0
517 | ```
518 |
519 | #### 4.4.1 跳过作用域
520 |
521 | 如果在嵌套的一个变量作用域里使用之前预定义的一个作用域,则会跳过当前变量的作用域,保持预先存在的作用域不变:
522 |
523 | ```python
524 | with tf.variable_scope('outside_scope') as outside_scope:
525 | print(outside_scope.name) # >> outside_scope
526 |
527 | with tf.variable_scope('first_scope') as first_scope:
528 | print(first_scope.name) # >> first_scope
529 | print(tf.get_variable('var', shape=[1, 2]).name) # >> first_scope/var:0
530 |
531 | with tf.variable_scope(outside_scope) as second_scope:
532 | print(second_scope.name) # >> outside_scope
533 | print(tf.get_variable('var', shape=[1, 2]).name) # >> outside_scope/var:0
534 | ```
535 |
536 | #### 4.4.2 多级变量作用域中的reuse
537 |
538 | 在多级变量作用域中,规定外层的变量作用域设置了`reuse=True`,内层的所有作用域的`reuse`必须设置为`True`(设置为其它无用)。通常的,要尽量避免出现嵌套不同种`reuse`属性的作用域出现,这是难以管理的。
539 |
540 | 多级变量作用域中,使用`tf.get_variable()`的方法如下:
541 |
542 | ```python
543 | # 定义
544 | with tf.variable_scope('s1') as s1:
545 | tf.get_variable('var', shape=[1,2])
546 | with tf.variable_scope('s2') as s2:
547 | tf.get_variable('var', shape=[1,2])
548 | with tf.variable_scope('s3') as s3:
549 | tf.get_variable('var', shape=[1,2])
550 |
551 | # 使用
552 | with tf.variable_scope('s1', reuse=True) as s1:
553 | v1 = tf.get_variable('var')
554 | with tf.variable_scope('s2', reuse=None) as s2:
555 | v2 = tf.get_variable('var')
556 | with tf.variable_scope('s3', reuse=None) as s3:
557 | v3 = tf.get_variable('var')
558 | ```
559 |
560 | ### 4.5 其它功能
561 |
562 | `variable_scope`可以设置其作用域内的所有变量的一系列默认操作,比如初始化方法与正则化方法等。通过设置默认值可以使得上下文中的代码简化,增强可读性。
563 |
564 | 例如,这里我们将初始化、正则化、变量数据类型等均使用默认值:
565 |
566 | ```python
567 | with tf.variable_scope('my_scope',
568 | initializer=tf.ones_initializer,
569 | regularizer=tf.keras.regularizers.l1(0.1),
570 | dtype=tf.float32):
571 | var = tf.get_variable('var', shape=[])
572 | reg = tf.losses.get_regularization_losses()
573 |
574 | with tf.Session() as sess:
575 | sess.run(tf.global_variables_initializer())
576 | print(sess.run([var, reg])) # >>> [1.0, [0.1]]
577 | ```
578 |
579 | 常见的初始化器有:
580 |
581 | ```python
582 | # 常数初始化器
583 | tf.constant_initializer(value=0, dtype=tf.float32)
584 | # 服从正态分布的随机数初始化器
585 | tf.random_normal_initializer(mean=0.0, stddev=1.0, seed=None, dtype=tf.float32)
586 | # 服从截断正态分布的随机数初始化器
587 | tf.truncated_normal_initializer(mean=0.0, stddev=1.0, seed=None, dtype=tf.float32)
588 | # 服从均匀分布的随机数初始化器
589 | tf.random_uniform_initializer(minval=0, maxval=None, seed=None, dtype=tf.float32)
590 | # 全0初始化器
591 | tf.zeros_initializer(dtype=tf.float32)
592 | # 全1初始化器
593 | tf.ones_initializer(dtype=tf.float32)
594 | ```
595 |
596 | 常用的范数正则化方法有(此处使用`tf.keras`模块下的方法,也可以使用`tf.contrib.layers`模块中的方法,但不推荐):
597 |
598 | ```python
599 | # 1范数正则化
600 | tf.keras.regularizers.l1(l=0.01)
601 | # 2范数正则化
602 | tf.keras.regularizers.l2(l=0.01)
603 | # 1范数2范数正则化
604 | tf.keras.regularizers.l1_l2(l1=0.01, l2=0.01)
605 | ```
606 |
607 | **小练习**:
608 |
609 | > 复现最后一小节代码,掌握`variable_scope`的一般用法。
610 |
611 | ## 作业
612 |
613 | 1. 总结`name_scope`与`variable_scope`的作用以及异同点。
614 | 2. 构建逻辑回归模型(只有模型部分,不包括训练部分),使用`get_variable`与`variable_scope`将变量的创建与使用分开。提示:使用`tf.nn.sigmoid`实现`logistic`函数。
--------------------------------------------------------------------------------
/ch06/ch6-IO.md:
--------------------------------------------------------------------------------
1 | 当我们已经训练好一个模型时,需要把模型保存下来,这样训练的模型才能够随时随地的加载与使用。模型的保存和加载分为两部分,一部分是模型定义的存取即**图的存取**,另一部分中图中的**变量的存取**(常量存储在图中)。TensorFlow可以分别完成两部分的内容的存取,也可以同时完成。除此以外TensorFlow还封装了很多path相关的操作,其类似于Python中的标准库`os.path`的功能。本章主要内容为基础文件操作,接下来的一章内容为模型存取操作。
2 |
3 | # 1. io模块
4 |
5 | io模块是简单的文件输入输出操作的模块,主要功能是读取文件中的内容并转化为张量以及解码数据。这里我们主要介绍一些常用的方法。
6 |
7 | ## 1.1 文件读写
8 |
9 | 使用`tf.io.read_file()`可根据`filename`将数据读取出来,其格式为TensorFlow中的`DT_STRING`,类似于Python中的`Bytes`,使用`tf.io.read_file`读取文件之后可使用相关的解码操作解码出数据来,例如读取一张图片,并解码:
10 |
11 | ```python
12 | f = tf.io.read_file('test.png')
13 | decode_img = tf.image.decode_png(f, channels=4)
14 | ```
15 |
16 | 此时`decode_img`即为图片对应的张量。
17 |
18 | 使用`tf.io.write_file()`可将`DT_STRING`写入文件中,例如将上述`f`重新写入文件:
19 |
20 | ```python
21 | tf.io.write_file('new.png', f)
22 | ```
23 |
24 | ## 1.2 解码数据
25 |
26 | 解码数据即将`DT_STRING`类型的数据转化为包含真实信息的原始数据的过程。不同类型的数据解码需要使用不同的解码方法,TensorFlow中解码数据的API分布在不同的模块中,`tf.io`模块包主要含以下几种解码方法:
27 |
28 | ```python
29 | tf.io.decode_base64 # 解码base64编码的数据,可解码使用tf.encode_base64编码的数据
30 | tf.io.decode_compressed # 解码压缩文件
31 | to.io.decode_json_example # 解码json数据
32 | tf.io.decode_raw # 解码张量数据
33 | ```
34 |
35 | # 2. gfile模块
36 |
37 | `tf.gfile`模块也是文件操作模块,其类似于Python中对file对象操作的API,但其与`tf.io`不同,`tf.gfile`模块中的API返回值并不是Tensor,而是Python原生数据。但TensorFlow的文件IO会调用C++的接口,实现文件操作。同时TensorFlow的`gfile`模块也支持更多的功能,包括操作本地文件、Hadoop分布式文件系统(HDFS)、谷歌云存储等。
38 |
39 | `tf.gfile`提供了丰富的文件相关操作的API,例如文件与文件夹的创建、修改、复制、删除、查找、统计等。这里我们简单介绍几种常用的文件操作方法。
40 |
41 | ## 2.1 打开文件流
42 |
43 | 打开并操作文件首先需要创建一个文件对象。这里有两种方法进行操作:
44 |
45 | - `tf.gfile.GFile`。也可以使用`tf.gfile.Open`,两者是等价的。此方法创建一个文件对象,返回一个上下文管理器。
46 |
47 | 用法如下:
48 |
49 | ```python
50 | tf.gfile.GFile(name, mode='r')
51 | ```
52 |
53 | 输入一个文件名进行操作。参数`mode`是操作文件的类型,有`"r", "w", "a", "r+", "w+", "a+"`这几种。分别代表只读、只写、增量写、读写、读写(包括创建)、可读可增量写。默认情况下读写操作都是操作的文本类型,如果需要写入或读取bytes类型的数据,就需要在类型后再加一个`b`。这里要注意的是,其与Python中文件读取的`mode`类似,但不存在`"t","U"`的类型(不加`b`就等价于Python中的`t`类型)。
54 |
55 | - `tf.gfile.FastGFile`。与`tf.gfile.GFile`用法、功能都一样(旧版本中`tf.gfile.FastGFile`支持无阻塞的读取,`tf.gfile.GFile`不支持。目前的版本都支持无阻塞读取)。一般的,使用此方法即可。
56 |
57 | 例如:
58 |
59 | ```python
60 | # 可读、写、创建文件
61 | with tf.gfile.GFile('test.txt', 'w+') as f:
62 | ...
63 |
64 | # 可以给test.txt追加内容
65 | with tf.gfile.Open('test.txt', 'a') as f:
66 | ...
67 |
68 | # 只读test.txt
69 | with tf.gfile.FastGFile('test.txt', 'r') as f:
70 | ...
71 |
72 | # 操作二进制格式的文件
73 | with tf.gfile.FastGFile('test.txt', 'wb+') as f:
74 | ...
75 | ```
76 |
77 | ## 2.2 数据读取
78 |
79 | 文件读取使用文件对象的`read`方法。(这里我们以`FastGFile`为例,与`GFile`一样)。文件读取时,会有一个指针指向读取的位置,当调用`read`方法时,就从这个指针指向的位置开始读取,调用之后,指针的位置修改到新的未读取的位置。`read`的用法如下:
80 |
81 | ```python
82 | # 返回str类型的内容
83 | tf.gfile.FastGFile.read(n=-1)
84 | ```
85 |
86 | 当参数`n=-1`时,代表读取整个文件。`n!=-1`时,代表读取`n`个bytes长度。
87 |
88 | 例如:
89 |
90 | ```python
91 | with tf.gfile.FastGFile('test.txt', 'r') as f:
92 | f.read(3) # 读取前3个bytes
93 | f.read() # 读取剩下的所有内容
94 | ```
95 |
96 | 如果我们需要修改文件指针,只读部分内容或跳过部分内容,可以使用`seek`方法。用法如下:
97 |
98 | ```python
99 | tf.gfile.FastGFile.seek(
100 | offset=None, # 偏移量 以字节为单位
101 | whence=0, # 偏移其实位置 0表示从文件头开始(正向) 1表示从当前位置开始(正向) 2表示从文件末尾开始(反向)
102 | position=None # 废弃参数 使用`offset`参数替代
103 | )
104 | ```
105 |
106 | 例如:
107 |
108 | ```python
109 | with tf.gfile.FastGFile('test.txt', 'r') as f:
110 | f.seed(3) # 跳过前3个bytes
111 | f.read() # 读取剩下的所有内容
112 | ```
113 |
114 | **注意**:读取文件时,默认的(不加`b`的模式)会对文件进行解码。会将bytes类型转换为UTF-8类型,如果读入的数据编码格式不是UTF-8类型,则在解码时会出错,这时需要使用二进制读取方法。
115 |
116 | 除此以外,还可以使用`readline`方法对文件进行读取。其可以读取以`\n`为换行符的文件的一行内容。例如:
117 |
118 | ```python
119 | with tf.gfile.FastGFile('test.txt', 'r') as f:
120 | f.readline() # 读取一行内容(包括行末的换行符)
121 | f.readlines() # 读取所有行,返回一个list,list中的每一个元素都是一行
122 | ```
123 |
124 | 以行为单位读取内容时,还可以使用`next`方法或是使用生成器来读取。如下:
125 |
126 | ```python
127 | with tf.gfile.FastGFile('test.txt', 'r') as f:
128 | f.next() # 读取下一行内容
129 |
130 | with tf.gfile.FastGFile('test.txt', 'r') as f:
131 | # 二进制数据首先会将其中的代表`\n`的字符转换为\n,然后会以\n作为分隔符生成list
132 | lines = [line for line in f]
133 | ```
134 |
135 | **注意**:如果没有使用with承载上下文管理器,文件读取完毕之后,需要显示的使用`close`方法关闭文件IO。
136 |
137 | ## 2.3 其它文件操作
138 |
139 | - **文件复制**:`tf.gfile.Copy(oldpath, newpath, overwrite=False)`
140 | - **删除文件**:`tf.gfile.Remove(filename)`
141 | - **递归删除**:`tf.gfile.DeleteRecursively(dirname)`
142 | - **判断路径是否存在**:`tf.gfile.Exists(filename)` # filename可指代路径
143 | - **判断路径是否为目录**:`tf.gfile.IsDirectory(dirname)`
144 | - **返回当前目录下的内容**:`tf.gfile.ListDirectory(dirname)` # 不递归 不显示'.'与'..'
145 | - **创建目录**:`tf.gfile.MkDir(dirname)` # 其父目录必须存在
146 | - **创建目录**:`tf.gfile.MakeDirs(dirname)` # 任何一级目录不存在都会进行创建
147 | - **文件改名**:`tf.gfile.Rename(oldname, newname, overwrite=False)`
148 | - **统计信息**:`tf.gfile.Stat(filename)`
149 | - **文件夹遍历**:`tf.gfile.Walk(top, in_order=True)` # 默认广度优先
150 | - **文件查找**:`tf.gfile.Glob(filename)` # 支持pattern查找
151 |
152 | **小练习:**
153 |
154 | > 练习上述文件相关操作,主要是使用`tf.gfile.FastGFile` API 读取数据。
155 |
156 |
--------------------------------------------------------------------------------
/ch07/ch7-queue-and-thread.md:
--------------------------------------------------------------------------------
1 | 在Python中,线程和队列的使用范围很广,例如可以将运行时间较长的代码放在线程中执行,避免页面阻塞问题,例如利用多线程和队列快速读取数据等。然而在Tensorflow中,线程与队列的使用场景基本上是读取数据。并且**在1.2之后的版本中使用了`tf.contrib.data`模块代替了多线程与队列读取数据的方法(为了兼容旧程序,多线程与队列仍然保留)。**
2 |
3 |
4 |
5 | ## 1. 队列
6 |
7 | Tensorflow中拥有队列的概念,用于操作数据,队列这种数据结构如在银行办理业务排队一样,队伍前面的人先办理,新进来的人排到队尾。队列本身也是图中的一个节点,与其相关的还有入队(enqueue)节点和出队(dequeue)节点。入队节点可以把新元素插入到队列末尾,出队节点可以把队列前面的元素返回并在队列中将其删除。
8 |
9 | 在Tensorflow中有两种队列,即FIFOQueue和RandomShuffleQueue,前者是标准的先进先出队列,后者是随机队列。
10 |
11 |
12 |
13 | ### 1.1 FIFOQueue
14 |
15 | FIFOQueue创建一个先进先出队列,这是很有用的,当我们需要加载一些有序数据,例如按字加载一段话,这时候我们不能打乱样本顺序,就可以使用队列。
16 |
17 | 创建先进先出队列使用`ft.FIFOQueue()`,具体如下:
18 |
19 | ~~~python
20 | tf.FIFOQueue(
21 | capacity, # 容量 元素数量上限
22 | dtypes,
23 | shapes=None,
24 | names=None,
25 | shared_name=None,
26 | name='fifo_queue')
27 | ~~~
28 |
29 | 如下,我们定义一个容量为3的,元素类型为整型的队列:
30 |
31 | ~~~python
32 | queue = tf.FIFOQueue(3, tf.int32)
33 | ~~~
34 |
35 | **注意**:`ft.FIFOQueue()`的参数`dtypes`、`shapes`、`names`均是列表(当列表的长度为1时,可以使用元素代替列表),且为对应关系,如果dtypes中包含两个dtype,则shapes中也包含两个shape,names也包含两个name。其作用是同时入队或出队多个有关联的元素。
36 |
37 | 如下,定义一个容量为3,每个队列值包含一个整数和一个浮点数的队列:
38 |
39 | ~~~python
40 | queue = tf.FIFOQueue(
41 | 3,
42 | dtypes=[tf.int32, tf.float32],
43 | shapes=[[], []],
44 | names=['my_int', 'my_float'])
45 | ~~~
46 |
47 | 队列创建完毕之后,就可以进行出队与入队操作了。
48 |
49 |
50 |
51 | #### 入队
52 |
53 | 入队即给队列添加元素。使用`quene.enqueue()`或`queue.enqueue_many()`方法,前者用来入队一个元素,后者用来入队0个或多个元素。如下:
54 |
55 | ~~~python
56 | queue = tf.FIFOQueue(3, dtypes=tf.float32)
57 | # 入队一个元素 简写
58 | eq1 = queue.enqueue(1.)
59 | # 入队一个元素 完整写法
60 | eq2 = queue.enqueue([1.])
61 | # 入队多个元素 完整写法
62 | eq3 = queue.enqueue_many([[1.], [2.]])
63 |
64 | with tf.Session() as sess:
65 | sess.run([eq1, eq2, eq3])
66 | ~~~
67 |
68 | 如果入队操作会导致元素数量大于队列容量,则入队操作会阻塞,直到出队操作使元素数量减少到容量值。
69 |
70 | 当我们指定了队列中元素的names时,我们在入队时需要使用使用字典来指定入队元素,如下:
71 |
72 | ~~~python
73 | queue = tf.FIFOQueue(
74 | 3,
75 | dtypes=[tf.float32, tf.int32],
76 | shapes=[[], []],
77 | names=['my_float', 'my_int'])
78 | queue.enqueue({'my_float': 1., 'my_int': 1})
79 | ~~~
80 |
81 |
82 |
83 | #### 出队
84 |
85 | 出队即给从队列中拿出元素。出队操作类似于入队操作`queue.dequeue()`、`queue.dequeue_many()`分别出队一个或多个元素,使用。如下:
86 |
87 | ~~~python
88 | queue = tf.FIFOQueue(3, dtypes=tf.float32)
89 | queue.enqueue_many([[1.], [2.], [3.]])
90 |
91 | val = queue.dequeue()
92 | val2 = queue.dequeue_many(2)
93 |
94 | with tf.Session() as sess:
95 | sess.run([val, val2])
96 | ~~~
97 |
98 | 如果队列中元素的数量不够出队操作所需的数量,则出队操作会阻塞,直到入队操作加入了足够多的元素。
99 |
100 |
101 |
102 | ### 1.2 RandomShuffleQueue
103 |
104 | RandomShuffleQueue创建一个随机队列,在执行出队操作时,将以随机的方式拿出元素。当我们需要打乱样本时,就可以使用这种方法。例如对小批量样本执行训练时,我们希望每次取到的小批量的样本都是随机组成的,这样训练的算法更准确,此时使用随机队列就很合适。
105 |
106 | 使用`tf.RandomShuffleQueue()`来创建随机队列,具体如下:
107 |
108 | ~~~python
109 | tf.RandomShuffleQueue(
110 | capacity, # 容量
111 | min_after_dequeue, # 指定在出队操作之后最少的元素数量,来保证出队元素的随机性
112 | dtypes,
113 | shapes=None,
114 | names=None,
115 | seed=None,
116 | shared_name=None,
117 | name='random_shuffle_queue')
118 | ~~~
119 |
120 | RandomShuffleQueue的出队与入队操作与FIFOQueue一样。
121 |
122 | 举例:
123 |
124 | ~~~python
125 | queue = tf.RandomShuffleQueue(10, 2, dtypes=tf.int16, seed=1)
126 |
127 | with tf.Session() as sess:
128 | for i in range(10):
129 | sess.run(queue.enqueue([i]))
130 | print('queue size is : %d ' % sess.run(queue.size()))
131 |
132 | for i in range(8):
133 | print(sess.run(queue.dequeue()))
134 | print('queue size is : %d ' % sess.run(queue.size()))
135 | queue.close()
136 | ~~~
137 |
138 | 执行结果如下:
139 |
140 | ```
141 | queue size is : 10
142 | 7
143 | 8
144 | 1
145 | 3
146 | 9
147 | 4
148 | 2
149 | 6
150 | queue size is : 2
151 | ```
152 |
153 | 本例中用到了`queue.size()`来获取队列的长度,使用`queue.close()`来关闭队列。可以看到顺序入队,得到的是乱序的数据。
154 |
155 |
156 |
157 | ## 2. 线程
158 |
159 | 在Tensorflow1.2之前的版本中,数据的输入,需要使用队列配合多线程。在之后的版本中将不再推荐使用这个功能,而是使用`tf.contrib.data`模块代替。
160 |
161 | Tensorflow中的线程调用的是Python的`threading`库。并增加了一些新的功能。
162 |
163 |
164 |
165 | ### 2.1 线程协调器
166 |
167 | Python中使用多线程,会有一个问题,即多个线程运行时,一个线程只能在运行完毕之后关闭,如果我们在线程中执行了一个死循环,那么线程就不存在运行完毕的状态,那么我们就无法关闭线程。例如我们使用多个线程给队列循环入队数据,那么我们就无法结束这些线程。线程协调器可以用来管理线程,让所有执行的线程停止运行。
168 |
169 | 线程协调器使用类`tf.train.Coordinator()`来管理。
170 |
171 | 如下:
172 |
173 | ~~~python
174 | import tensorflow as tf
175 | import threading
176 |
177 | # 将要在线程中执行的函数
178 | # 传入一个Coordinator对象
179 | def myloop(coord):
180 | while not coord.should_stop():
181 | ...do something...
182 | if ...some condition...:
183 | coord.request_stop()
184 | # 创建Coordinator对象
185 | coord = tf.train.Coordinator()
186 | # 创建10个线程
187 | threads = [threading.Thread(target=myLoop, args=(coord,)) for _ in range(10)]
188 | # 开启10个线程
189 | for t in threads:
190 | t.start()
191 | # 等待线程执行结束
192 | coord.join(threads)
193 | ~~~
194 |
195 | 上述代码需要注意的是`myloop`函数中,根据`coord.should_stop()`的状态来决定是否运行循环体,默认情况下`coord.should_stop()`返回`False`。当某个线程中执行了`coord.request_stop()`之后,所有线程在执行循环时,都会因为`coord.should_stop()`返回`True`而终止执行,从而使所有线程结束。
196 |
197 |
198 |
199 | ### 2.2 队列管理器
200 |
201 | 上面队列的例子中,出队、入队均是在主线程中完成。这时候会有一个问题:当入队操作速度较慢,例如加载较大的数据时,入队操作不仅影响了主线程的总体速度,也会造成出队操作阻塞,更加拖累了线程的执行速度。我们希望入队操作能够在一些列新线程中执行,主进程仅仅执行出队和训练任务,这样就可以提高整体的运行速度。
202 |
203 | 如果直接使用Python中的线程进行管理Tensorflow的队列会出一些问题,例如子线程无法操作主线程的队列。在Tensorflow中,队列管理器可以用来创建、运行、管理队列线程。队列管理器可以管理一个或多个队列。
204 |
205 |
206 |
207 | #### 单队列管理
208 |
209 | 用法如下:
210 |
211 | 1. 使用`tf.train.QueueRunner()`创建一个队列管理器。队列管理器中需要传入队列以及队列将要执行的操作和执行的线程数量。
212 | 2. 在会话中开启所有的队列管理器中的线程。
213 | 3. 使用Coordinator通知线程结束。
214 |
215 | 例如:
216 |
217 | ~~~python
218 | g = tf.Graph()
219 | with g.as_default():
220 | q = tf.FIFOQueue(100, tf.float32)
221 | enq_op = q.enqueue(tf.constant(1.))
222 |
223 | # 为队列q创建一个拥有两个线程的队列管理器
224 | num_threads = 2
225 | # 第一个参数必须是一个队列,第二个参数必须包含入队操作
226 | qr = tf.train.QueueRunner(q, [enq_op] * num_threads)
227 |
228 | with tf.Session(graph=g) as sess:
229 | coord = tf.train.Coordinator()
230 | # 在会话中开启线程并使用coord协调线程
231 | threads = qr.create_threads(sess, start=True, coord=coord)
232 |
233 | deq_op = q.dequeue()
234 | for i in range(15):
235 | print(sess.run(deq_op))
236 |
237 | # 请求关闭线程
238 | coord.request_stop()
239 | coord.join(threads)
240 | ~~~
241 |
242 |
243 |
244 | #### 多队列管理
245 |
246 | 队列管理器还可以管理多个队列,例如可以创建多个单一的队列管理器分别进行操作。但为了统一管理多个队列,简化代码复杂度,Tensorflow可以使用多队列管理。即创建多个队列管理器,统一开启线程和关闭队列。
247 |
248 | 使用方法如下:
249 |
250 | 1. 与单队列意义,使用`tf.train.QueueRunner()`创建一个队列管理器(也可以使用`tf.train.queue_runner.QueueRunner()`创建队列管理器,两个方法一模一样)。一个队列管理器管理一个队列。队列管理器中需要传入将要执行的操作以及执行的线程数量。例如:
251 |
252 | ~~~python
253 | # 队列1
254 | q1 = tf.FIFOQueue(100, dtypes=tf.float32)
255 | enq_op1 = q1.enqueue([1.])
256 | qr1 = tf.train.QueueRunner(q1, [enq_op1] * 4)
257 |
258 | # 队列2
259 | q2 = tf.FIFOQueue(50, dtypes=tf.float32)
260 | enq_op2 = q2.enqueue([2.])
261 | qr2 = tf.train.QueueRunner(q2, [enq_op2] * 3)
262 | ~~~
263 |
264 | 2. `tf.train.add_queue_runner()`向图的队列管理器集合中添加一个队列管理器(也可以使用`tf.train.queue_runner.add_queue_runner()`添加队列管理器,两个方法一模一样)。一个图可以有多个队列管理器。例如:
265 |
266 | ~~~python
267 | tf.train.add_queue_runner(qr1)
268 | tf.train.add_queue_runner(qr2)
269 | ~~~
270 |
271 | 3. `tf.train.start_queue_runners()`在会话中开启所有的队列管理器(也可以使用`tf.train.queue_runner.start_queue_runners()`开启,两个方法一模一样),即开启线程。这时候,我们可以将线程协调器传递给它,用于完成线程执行。例如:
272 |
273 | ~~~python
274 | with tf.Session() as sess:
275 | coord = tf.train.Coordinator()
276 | threads = tf.train.start_queue_runners(sess, coord=coord)
277 | ~~~
278 |
279 | 4. 任务完成,结束线程。使用Coordinator来完成:
280 |
281 | *注意:结束线程的这些操作还用到了Session,应该放在Session上下文中,否则可能出现Session关闭的错误。*
282 |
283 | ~~~python
284 | coord.request_stop()
285 | coord.join(threads)
286 | ~~~
287 |
288 | 完整案例:
289 |
290 | 在图中创建了一个队列,并且使用队列管理器进行管理。创建的队列
291 |
292 | ~~~python
293 | g = tf.Graph()
294 | with g.as_default():
295 | # 创建队列q1
296 | q1 = tf.FIFOQueue(10, tf.int32)
297 | enq_op1 = q1.enqueue(tf.constant(1))
298 | qr1 = tf.train.QueueRunner(q1, [enq_op1] * 3)
299 | tf.train.add_queue_runner(qr1)
300 |
301 | # 创建队列q2
302 | q2 = tf.FIFOQueue(10, tf.int32)
303 | enq_op2 = q2.enqueue(tf.constant(2))
304 | qr2 = tf.train.QueueRunner(q2, [enq_op2] * 3)
305 | tf.train.add_queue_runner(qr2)
306 |
307 | with tf.Session(graph=g) as sess:
308 | coord = tf.train.Coordinator()
309 | threads = tf.train.start_queue_runners(sess, coord=coord)
310 |
311 | my_data1 = q1.dequeue()
312 | my_data2 = q2.dequeue()
313 |
314 | for _ in range(15):
315 | print(sess.run(my_data1))
316 | print(sess.run(my_data2))
317 |
318 | coord.request_stop()
319 | coord.join(threads)
320 | ~~~
321 |
322 |
323 |
324 | #### 队列中op的执行顺序
325 |
326 | 队列管理器中会指定入队op,但有些时候入队op执行时,还需要同时执行一些别的操作,例如我们希望按顺序生成一个自然数队列,我们每次入队时,需要对入队的变量加1。然而如下写法输出的结果并没有按照顺序来:
327 |
328 | ~~~python
329 | q = tf.FIFOQueue(100, tf.float32)
330 | counter = tf.Variable(tf.constant(0.))
331 |
332 | assign_op = tf.assign_add(counter, tf.constant(1.))
333 | enq_op = q.enqueue(counter)
334 |
335 | # 使用一个线程入队
336 | qr = tf.train.QueueRunner(q, [assign_op, enq_op] * 1)
337 |
338 | with tf.Session() as sess:
339 | sess.run(tf.global_variables_initializer())
340 | qr.create_threads(sess, start=True)
341 |
342 | for i in range(10):
343 | print(sess.run(q.dequeue()))
344 | # 输出结果
345 | # 1.0
346 | # 3.0
347 | # 4.0
348 | # 4.0
349 | # 5.0
350 | # 7.0
351 | # 9.0
352 | # 10.0
353 | # 12.0
354 | # 12.0
355 | ~~~
356 |
357 | 可以看到`assign_op`与`enq_op`的执行是异步的,入队操作并没有严格的在赋值操作之后执行。
358 |
359 |
--------------------------------------------------------------------------------
/ch08/ch8-dataset-01.md:
--------------------------------------------------------------------------------
1 | 在机器学习中,文件读取是最常见的也较复杂的操作之一。通常,我们训练一个模型或者部署运行一个模型均需要对文件进行读写,例如获取训练数据集与保存训练模型。保存模型之前我们已经讲过了,这一节主要讲对训练数据集的读取操作。
2 |
3 | Tensorflow读取训练数据主要有以下三种方法:
4 |
5 | * feeding:即每次运行算法时,从Python环境中供给数据(不允许Tensor数据传入)。
6 | * 常量:在图中设置一些常量进行保存数据。这仅仅适合数据量较小的情况。
7 | * 文件读取。直接从文件中读取。
8 |
9 | 机器学习中训练模型使用的数据集通常比较大,有些数据往往存储在多个文件中,而另一些数据则根据其功能写入不同的文件中,这时候从文件中读取比较方便。例如MNIST中包含4个文件。如下:
10 |
11 | ~~~wiki
12 | train-images-idx3-ubyte.gz: training set images (9912422 bytes)
13 | train-labels-idx1-ubyte.gz: training set labels (28881 bytes)
14 | t10k-images-idx3-ubyte.gz: test set images (1648877 bytes)
15 | t10k-labels-idx1-ubyte.gz: test set labels (4542 bytes)
16 | ~~~
17 |
18 | 第一个文件是MNIST的训练集中的图片,第二个文件是训练集中的标记,第三个文件是测试集中的图片,第四个文件是测试集中的标记。
19 |
20 | 这样的好处是数据集的数据结构清晰。但有时候这样也会加大我们获取数据的难度,例如我们必须同时读取不同的文件来获得一条完整的数据样本,例如我们需要设置复杂的多线程加快数据读取。而Tensorflow提供了读取数据的便捷方法。
21 |
22 |
23 |
24 | ## 1. 文件读取数据的流程
25 |
26 | 从文件中读取数据是最常见的数据读取的方法。这里的“文件”是指文本文件、二进制文件等。从文件中读取数据可以配合之前所学的队列与线程的知识,高效的完成工作。
27 |
28 | 这里我们首先介绍从文件中读取数据的方法,之后再去结合线程与队列进行使用。
29 |
30 |
31 |
32 | ### 1.1 文件读取
33 |
34 | 文件读取可以分为两种情况,第一种是直接读取文件,第二种是从文件名队列中读取数据。对于文件较多的数据集,我们通常采用第二种方法。
35 |
36 | #### 1.1.1 直接读取文件
37 |
38 | 使用`tf.read_file`对文件进行读取,返回一个`string`类型的Tensor。
39 |
40 | ~~~python
41 | tf.read_file(filename, name=None)
42 | ~~~
43 |
44 | 一个数据结构如下的csv文件,其读取的结果是:`b'1,11\n2,22\n3,33\n'`
45 |
46 | ~~~tex
47 | 1,11
48 | 2,22
49 | 3,33
50 | ~~~
51 |
52 | 可以看到,这种方法仅仅适合读取一个文件就是一个样本(或特征)的数据,例如图片。由于解码不方便,其不适合读取文本文件或其它文件。
53 |
54 | #### 1.1.2 从文件名队列中读取
55 |
56 | 为什么我们需要从文件名队列中读取呢?因为随着样本数据规模的增大,文件的数量急剧增多,如何管理文件就成为了一个问题。例如,我们拥有100个文件,我们希望读取乱序读取文件生成5代(epoch)样本,直接管理文件就很麻烦。使用队列,就可以解决这个问题。
57 |
58 | 从文件名队列中读取文件有2个步骤。
59 |
60 | ##### 步骤一:生成文件名队列
61 |
62 | Tensorflow为我们提供了文件名队列`tf.train.string_input_producer`用于管理被读取的文件。
63 |
64 | ~~~python
65 | # 返回一个队列,同时将一个QueueRunner加入图的`QUEUE_RUNNER`集合中
66 | tf.train.string_input_producer(
67 | string_tensor, # 1维的string类型的文件名张量 可以是文件路径
68 | num_epochs=None, # 队列的代,设置了之后,超出队列会出现OutOfRange错误,且需要使用local_variables_initializer()初始化
69 | shuffle=True, # True代表每一个代中的文件名是打乱的
70 | seed=None, # 随机种子
71 | capacity=32, # 队列容量
72 | shared_name=None,
73 | name=None,
74 | cancel_op=None)
75 | ~~~
76 |
77 | **注意:`local_variables_initializer()`需要写在用到local_variable的地方的后面。这与`global_variables_initializer()`用法不太一样。**
78 |
79 | 例如,生成两个csv文件名队列:
80 |
81 | ~~~python
82 | filename_queue = tf.train.string_input_producer(['1.csv', '2.csv'])
83 | ~~~
84 |
85 | 或者我们可以使用列表推导来生成文件名队列,例如:
86 |
87 | ~~~python
88 | filename_queue = tf.train.string_input_producer(['%d.csv' % i for i in range(1, 3)])
89 | ~~~
90 |
91 | 对于简单的、文件较少的数据集,使用上述方法生成文件名队列很合适。但面对大型的数据集,这样操作就不够灵活,例如中文语音语料库thchs30训练数据集的音频文件有一个目录A2中的文件如下:
92 |
93 | 
94 |
95 | 这个数据集目录中包含类了似于A2这样的目录就有几十个,每个目录中都有200个左右的文件。这些文件的名称都有一定的规律,例如A2目录下的文件的文件名开头都是'A2',结尾都是'.wav'中间是不连续的1-n的自然数。我们无法使用列表推导`['A2_%d.wav' % i for i in range(n)]`(因为不连续)。
96 |
97 | Tensorflow提供了获取文件名的模式匹配的方法`tf.train.match_filenames_once`。可以灵活的使用此方法获取我们所要的文件名,过滤掉不需要的文件的文件名。用法如下:
98 |
99 | ```python
100 | tf.train.match_filenames_once(
101 | pattern, # 文件名匹配模式(glob,可以看做是简化版的正则表达式)或者一个文件名Tensor
102 | name=None
103 | )
104 | ```
105 |
106 | glob是Linux内建的用于文件路径查找的函数,glob通配符类型有:
107 |
108 | - `*` :任意长度的任意字符
109 | - `?` :任意单个字符
110 | - `[]` :匹配指定范围内的单个字符
111 | - `[0-9]` :单个数字
112 | - `[a-z]` :不区分大小写的a-z
113 | - `[A-Z]` :大写字符
114 | - `[^]` :匹配指定范围外的单个字符
115 |
116 | 使用`tf.train.match_filenames_once`获取文件名队列的代码如下:
117 |
118 | ```python
119 | filenames = tf.train.match_filenames_once('A2_*.wav') # 这里也可以直接写 '*'
120 | ```
121 |
122 | 然后可以根据所有文件名`filenames`生成文件名队列:
123 |
124 | ~~~python
125 | filename_queue = tf.train.string_input_producer(filenames)
126 | ~~~
127 |
128 | ##### 步骤二:根据文件名队列读取相应文件
129 |
130 | 文件名队列出队得到的是文件名(也可以是文件路径),读取数据正是从这些文件名中找到文件并进行数据读取的。但需要注意的是,Tensorflow实现了配合“文件名出队”操作的数据读取op,所以我们并不需要写出队操作与具体的文件读取方法。同时读取数据的方法也有很多种,这里我们以读取csv文件为例,下文中,我们给出了更多种文件的读取方法。
131 |
132 | 读取csv文件,需要使用**文本文件读取器**`tf.TextLineReader`,与csv解码器一起工作。
133 |
134 | [CSV文件](https://tools.ietf.org/html/rfc4180)就是以逗号进行分割值,以\\n为换行符的文本文件,文件的后缀名为.csv。
135 |
136 | `tf.TextLineReader`用法如下:
137 |
138 | ```python
139 | # 创建一个读取器对象
140 | # 输出由换行符分割的文件读取器 可以用来读取csv、text等文本文件
141 | tf.TextLineReader(skip_header_lines=None, name=None)
142 |
143 | # 从文件名队列中读取数据 并返回下一个`record`(key, value)
144 | # 相当于出队操作
145 | tf.TextLineReader.read(filename_queue, name=None)
146 | ```
147 |
148 | 读取数据之后,我们还需要对数据进行解码,将其转变为张量,CSV文件的解码方法如下:
149 |
150 | ```python
151 | # 将csv文件解码成为Tensor
152 | tf.decode_csv(
153 | records,
154 | record_defaults, # 默认值,当 records 中有缺失值时,使用默认值填充
155 | field_delim=',', # 分隔符
156 | use_quote_delim=True, # 当为True时,会去掉元素的引号,为False时,会保留元素的引号
157 | name=None)
158 | ```
159 |
160 |
161 |
162 | 继续步骤一中未完成的例子,读取数据的操作如下:
163 |
164 | ~~~python
165 | reader = tf.TextLineReader()
166 | key, value = reader.read(filename_queue) # filename_queue是第一步生成的文件名队列
167 |
168 | decoded = tf.decode_csv(value, record_defaults) # record_defaults是必填项,此处省略了。
169 | ~~~
170 |
171 | 这里得到的key是文件名与行索引,value是key对应的张量值。key与value均为`tf.string`类型。
172 |
173 | 到这里,我们已经通过文件名队列获取到了数据,完成了我们的目标。
174 |
175 |
176 |
177 | ### 1.2 利用队列存取数据
178 |
179 | 虽然我们已经读取并解码了文件,但直接使用解码后的张量依然有很多的问题。例如我们希望使用“批量样本”,而其产生的是单个样本;例如当生成样本的速度与消费样本的速度不匹配时,可能造成阻塞。这些问题,我们可以使用队列来解决。
180 |
181 | 上一章,我们使用了队列对张量数据进行存储与读取。使用队列最大的优点就是可以充分发挥生产者-消费者的模式。而上文中,我们已经把文件读取并转换为了张量,那么我们就可以使用队列来管理读取的数据。
182 |
183 | 我们仍以读取csv文件为例进行演示:
184 |
185 | ~~~python
186 | g = tf.Graph()
187 | with g.as_default():
188 | # 生成“文件名队列”
189 | filenames = tf.train.match_filenames_once('./csv/*.csv')
190 | filename_queue = tf.train.string_input_producer(filenames)
191 |
192 | # 读取数据
193 | reader = tf.TextLineReader()
194 | key, value = reader.read(filename_queue)
195 |
196 | # 解码csv文件 record_default的值根据实际情况写
197 | decoded = tf.decode_csv(value, record_defaults=[[0], [0]])
198 |
199 | # 创建“样本队列” 这里容量与类型需要根据实际情况填写
200 | example_queue = tf.FIFOQueue(5, tf.int32)
201 | # 入队操作
202 | enqueue_example = example_queue.enqueue([decoded])
203 | # 出队操作 根据需要也可以dequeue_many
204 | dequeue_example = example_queue.dequeue()
205 |
206 | # 创建队列管理器 根据需要制定线程数量num_threads,这里为1
207 | qr = tf.train.QueueRunner(example_queue, [enqueue_example] * 1)
208 | # 将qr加入图的QueueRunner集合中
209 | tf.train.add_queue_runner(qr)
210 |
211 | with tf.Session(graph=g) as sess:
212 | # 创建线程协调器
213 | sess.run(tf.local_variables_initializer())
214 | coord = tf.train.Coordinator()
215 | threads = tf.train.start_queue_runners(sess=sess, coord=coord)
216 | # 出队数据
217 | for i in range(15):
218 | print(sess.run(dequeue_example))
219 |
220 | # 停止
221 | coord.request_stop()
222 | coord.join(threads)
223 | ~~~
224 |
225 | cuo
226 |
227 | 上面的代码就是完整的从文件中读取数据,并存在队列中的例子。这个例子中使用了2个队列,分别是:
228 |
229 | * 文件名队列
230 | * 数据读取与生成队列
231 |
232 | 通过使用队列,使得数据的读取与使用变得简便与条理。但可以看到上面的代码仍然较繁琐,Tensorflow提供了更加简便的API来简化操作,和完善功能。
233 |
234 |
235 |
236 | ### 1.3 简便的批量样本生成
237 |
238 | 上面的读取文件的例子中,使用了2个队列,其中“文件名队列”与“样本数据读取队列”已经被Tensorflow抽象成了简便的API,即我们不需要显式的创建队列和创建对应的队列管理器。其实第三个队列“样本队列”,Tensorflow也给出了更加简便的API,这些API的用法如下:
239 |
240 | 注意:以下几个API均可以完成样本生成,但功能略微不同。
241 |
242 | * `tf.train.batch`
243 |
244 | API的详情如下:
245 |
246 | ~~~python
247 | # 返回一个dequeue的OP,这个OP生成batch_size大小的tensors
248 | tf.train.batch(
249 | tensors, # 用于入队的张量
250 | batch_size, # 出队的大小
251 | num_threads=1, # 用于入队的线程数量
252 | capacity=32, # 队列容量
253 | enqueue_many=False, # 入队的样本是否为多个
254 | shapes=None, # 每个样本的shapes 默认从tensors中推断
255 | dynamic_pad=False, # 填充一个批次中的样本,使之shape全部相同
256 | allow_smaller_final_batch=False, # 是否允许最后一个批次的样本数量比正常的少
257 | shared_name=None,
258 | name=None)
259 | ~~~
260 |
261 | 在读取数据并解码之后,可以将张量送入此方法。
262 |
263 | 读取文件时,只需要将上面的例子中的批量样本生成代码替换为此即可,例如:
264 |
265 | ~~~python
266 | ...
267 | decoded = tf.decode_csv(...)
268 |
269 | dequeue_example = tf.train.batch(decoded, batch_size=5)
270 | ~~~
271 |
272 | 比之前少些了4行代码。更加简洁明了。
273 |
274 |
275 |
276 | * `tf.train.batch_join`
277 |
278 | API的详情如下:
279 |
280 | ~~~python
281 | # 返回一个dequeue的OP,这个OP生成batch_size大小的tensors
282 | tf.train.batch_join(
283 | tensors_list, # 入队
284 | batch_size,
285 | capacity=32,
286 | enqueue_many=False,
287 | shapes=None,
288 | dynamic_pad=False,
289 | allow_smaller_final_batch=False,
290 | shared_name=None,
291 | name=None)
292 | ~~~
293 |
294 | 此方法与上述方法类似,但是创建多线程的方法不同。这里是根据tensors_list的长度来决定线程的数量的。用法如下:
295 |
296 | ~~~python
297 | ...
298 | decoded_list = [tf.decode_csv(...) for _ in range(2)]
299 |
300 | dequeue_example = tf.train.batch_join(decoded_list, batch_size=5)
301 | ~~~
302 |
303 | 这里创建了2个解码器,在数据读取时,会分别给这两个解码op创建各自的线程。本质上与上面的方法是一样。
304 |
305 | * `tf.train.shuffle_batch`
306 |
307 | 与`tf.train.batch`相比,此方法可以打乱输入样本。API详情如下:
308 |
309 | ~~~python
310 | tf.train.shuffle_batch(
311 | tensors,
312 | batch_size,
313 | capacity,
314 | min_after_dequeue,
315 | num_threads=1,
316 | seed=None, # 随机数种子
317 | enqueue_many=False,
318 | shapes=None,
319 | allow_smaller_final_batch=False,
320 | shared_name=None,
321 | name=None)
322 | ~~~
323 |
324 | 用法与`tf.train.batch`类似。
325 |
326 | * `tf.train.shuffle_batch_join`
327 |
328 | 与`tf.train.batch_join`相比,此方法可以打乱输入样本。API详情如下:
329 |
330 | ~~~python
331 | tf.train.shuffle_batch(
332 | tensors,
333 | batch_size,
334 | capacity,
335 | min_after_dequeue,
336 | num_threads=1,
337 | seed=None,
338 | enqueue_many=False,
339 | shapes=None,
340 | allow_smaller_final_batch=False,
341 | shared_name=None,
342 | name=None)
343 | ~~~
344 |
345 | 用法与`tf.train.batch_join`类似。
346 |
347 |
348 |
349 |
350 | ### 1.4 完整读取数据一般步骤
351 |
352 | 从文件中读取数据的一般步骤如下:
353 |
354 | 1. 生成文件名队列。
355 | 2. 读取文件,生成样本队列。
356 | 3. 从样本队列中生成批量样本。
357 |
358 | 
359 |
360 |
361 |
362 | 例子:
363 |
364 | ~~~python
365 | # 第一步 生成文件名队列
366 | filename_queue = tf.train.string_input_producer(['1.csv', '2.csv'])
367 |
368 | # 第二步 根据文件名读取数据
369 | reader = tf.TextLineReader(filename_queue)
370 | record = reader.read(filename_queue)
371 | # 解码数据成为Tensor
372 | csv_tensor = tf.decode_csv(record, default_record=[...])
373 |
374 | # 第三步 根据Tensors生产批量样本
375 | batch_example = tf.train.batch(csv_tensor, batch_size)
376 | ~~~
377 |
378 |
379 |
380 | ## 2. 高效读取数据
381 |
382 | 上面的例子,我们直接从原始文件中读取数据。当文件数量较多,且解码不够迅速时,上面的方法就显示出了一些缺陷。在Tensorflow中,我们可以使用TFRecord对数据进行存取。TFRecord是一种二进制文件。可以更快速的操作文件。
383 |
384 | 通常我们得到的数据集并不是TFRecord格式,例如MNIST数据集也是一个二进制文件,每一个字节都代表一个像素值(除去开始的几个字节外)或标记,这与TFRecord文件的数据表示方法(TFRecord的二进制数据中还包含了校验值等数据)并不一样。所以,通常我们需要将数据转化为TFRecord文件。这里需要注意并不是每一个数据集均需要转化为TFRecord文件,建议将文件数量较多,直接读取效率低下的数据集转化为TFRecord文件格式。
385 |
386 |
387 |
388 | ### 2.1 写入TFRecord文件
389 |
390 | TFRecord文件的存取,本质上是对生成的包含样本数据的ProtoBuf数据的存取,TFRecord文件只适合用来存储数据样本。一个TFRecord文件存储了一个或多个example对象,example.proto文件描述了一个样本数据遵循的格式。每个样本example包含了多个特征feature,feature.proto文件描述了特征数据遵循的格式。
391 |
392 | 在了解如何写入TFRecord文件前,我们首先了解一下其对应的消息定义文件。通过这个文件,我们可以知道消息的格式。
393 |
394 | **feature.proto**的内容如下(删除了大部分注释内容):
395 |
396 | ~~~protobuf
397 | syntax = "proto3";
398 | option cc_enable_arenas = true;
399 | option java_outer_classname = "FeatureProtos";
400 | option java_multiple_files = true;
401 | option java_package = "org.tensorflow.example";
402 |
403 | package tensorflow;
404 |
405 | // Containers to hold repeated fundamental values.
406 | message BytesList {
407 | repeated bytes value = 1;
408 | }
409 | message FloatList {
410 | repeated float value = 1 [packed = true];
411 | }
412 | message Int64List {
413 | repeated int64 value = 1 [packed = true];
414 | }
415 |
416 | // Containers for non-sequential data.
417 | message Feature {
418 | // Each feature can be exactly one kind.
419 | oneof kind {
420 | BytesList bytes_list = 1;
421 | FloatList float_list = 2;
422 | Int64List int64_list = 3;
423 | }
424 | };
425 |
426 | message Features {
427 | // Map from feature name to feature.
428 | map feature = 1;
429 | };
430 |
431 | message FeatureList {
432 | repeated Feature feature = 1;
433 | };
434 |
435 | message FeatureLists {
436 | // Map from feature name to feature list.
437 | map feature_list = 1;
438 | };
439 | ~~~
440 |
441 | 可以看到一个特征`Feature`可以是3中数据类型(`BytesList`,`FloatList`,`Int64List`)之一。多个特征`Feature`组成一个组合特征`Features`,多个特征列表组成特征列表组`FeatureLists`。
442 |
443 | **example.proto**的内容如下(删除了大部分注释内容):
444 |
445 | ```protobuf
446 | syntax = "proto3";
447 |
448 | import "tensorflow/core/example/feature.proto";
449 | option cc_enable_arenas = true;
450 | option java_outer_classname = "ExampleProtos";
451 | option java_multiple_files = true;
452 | option java_package = "org.tensorflow.example";
453 |
454 | package tensorflow;
455 |
456 | message Example {
457 | Features features = 1;
458 | };
459 |
460 | message SequenceExample {
461 | Features context = 1;
462 | FeatureLists feature_lists = 2;
463 | };
464 | ```
465 |
466 | 可以看到一个样本`Example`包含一个特征组合。序列样本`SequenceExample`包含一个类型是特征组合的上下文`context`与一个特征列表组`feature_lists`。
467 |
468 | 可以看到:**TFRecord存储的样本数据是以样本为单位的。**
469 |
470 | 了解了TFRecord读写样本的数据结构之后,我们就可以使用相关API进行操作。
471 |
472 | #### 写入数据
473 |
474 | Tensorflow已经为我们封装好了操作protobuf的方法以及文件写入的方法。写入数据的第一步是打开文件并创建writer对象,Tensorflow使用`tf.python_io.TFRecordWriter`来完成,具体如下:
475 |
476 | ~~~python
477 | # 传入一个路径,返回一个writer上下文管理器
478 | tf.python_io.TFRecordWriter(path, options=None)
479 | ~~~
480 |
481 | TFRecordWriter拥有`write`,`flush`,`close`方法,分别用于写入数据到缓冲区,将缓冲区数据写入文件并清空缓冲区,关闭文件流。
482 |
483 | 开启文件之后,需要创建样本对象并将example数据写入。根据上面的proto中定义的数据结构,我们知道一个样本对象包含多个特征。所以我们首先需要创建特征对象,然后再创建样本对象。如下为序列化一个样本的例子:
484 |
485 | ~~~python
486 | with tf.python_io.TFRecordWriter('./test.tfrecord') as writer:
487 | f1 = tf.train.Feature(int64_list=tf.train.Int64List(value=[i]))
488 | f2 = tf.train.Feature(float_list=tf.train.FloatList(value=[1. , 2.]))
489 | b = np.ones([i]).tobytes() # 此处默认为float64类型
490 | f3 = tf.train.Feature(bytes_list=tf.train.BytesList(value=[b]))
491 |
492 | features = tf.train.Features(feature={'f1': f1, 'f2': f2, 'f3': f3})
493 | example = tf.train.Example(features=features)
494 |
495 | writer.write(example.SerializeToString())
496 | ~~~
497 |
498 | 序列化多个样本只需要重复上述的写入过程即可。如下:
499 |
500 | ~~~python
501 | with tf.python_io.TFRecordWriter('./test.tfrecord') as writer:
502 | # 多个样本多次写入
503 | for i in range(1, 6):
504 | f1 = tf.train.Feature(int64_list=tf.train.Int64List(value=[i]))
505 | f2 = tf.train.Feature(float_list=tf.train.FloatList(value=[1. , 2.]))
506 | b = np.ones([i]).tobytes()
507 | f3 = tf.train.Feature(bytes_list=tf.train.BytesList(value=[b]))
508 |
509 | features = tf.train.Features(feature={'f1': f1, 'f2': f2, 'f3': f3})
510 | example = tf.train.Example(features=features)
511 |
512 | writer.write(example.SerializeToString())
513 | ~~~
514 |
515 | **注意事项:**
516 |
517 | * `tf.train.Int64List`、`tf.train.FloatList`、`tf.train.BytesList`均要求输入的是python中的list类型的数据,而且list中的元素分别只能是int、float、bytes这三种类型。
518 | * 由于生成protobuf数据对象的类中,只接受关键字参数,所以参数必须写出参数名。
519 | * protobuf数据对象类需要遵循proto文件中定义的数据结构来使用。
520 |
521 |
522 |
523 | TFRecord文件的数据写入是在Python环境中完成的,不需要启动会话。写入数据的过程可以看做是原结构数据转换为python数据结构,再转换为proto数据结构的过程。完整的数据写入过程如下:
524 |
525 | 1. 读取文件中的数据。
526 | 2. 组织读取到的数据,使其成为以“样本”为单位的数据结构。
527 | 3. 将“样本”数据转化为Python中的数据结构格式(int64\_list,float\_list,bytes\_list三种之一)。
528 | 4. 将转化后的数据按照proto文件定义的格式写出“Example”对象。
529 | 5. 将“Example”对象中存储的数据序列化成为二进制的数据。
530 | 6. 将二进制数据存储在TFRecord文件中。
531 |
532 |
533 |
534 |
535 | ### 2.2 读取TFRecord文件
536 |
537 | 读取TFRecord文件类似于读取csv文件。只不过使用的是`tf.TFRecordReader`进行读取。除此以外,还需要对读取到的数据进行解码。`tf.TFRecordReader`用法如下:
538 |
539 | ~~~python
540 | reader = tf.TFRecordReader(name=None, options=None)
541 | key, value = reader.read(queue, name=None)
542 | ~~~
543 |
544 | 此处读取到的value是序列化之后的proto样本数据,我们还需要对数据进行解析,这里可以使用方法`tf.parse_single_example`。解析同样需要说明解析的数据格式,这里可以使用`tf.FixedLenFeature`与`tf.VarLenFeature`进行描述。
545 |
546 | 解析单个样本的方法如下:
547 |
548 | ~~~python
549 | # 解析单个样本
550 | tf.parse_single_example(serialized, features, name=None, example_names=None)
551 | ~~~
552 |
553 | 解析设置数据格式的方法如下
554 |
555 | ~~~python
556 | # 定长样本
557 | tf.FixedLenFeature(shape, dtype, default_value=None)
558 | # 不定长样本
559 | tf.VarLenFeature(dtype)
560 | ~~~
561 |
562 | 完整例子如下(解析的是上文中写入TFRecord文件的例子中生成的文件):
563 |
564 | ~~~python
565 | reader = tf.TFRecordReader()
566 | key, value = reader.read(filename_queue)
567 |
568 | example = tf.parse_single_example(value, features={
569 | 'f1': tf.FixedLenFeature([], tf.int64),
570 | 'f2': tf.FixedLenFeature([2], tf.float32),
571 | 'f3': tf.FixedLenFeature([], tf.string)})
572 |
573 | feature_1 = example['f1']
574 | feature_2 = example['f2']
575 | feature_3 = tf.decode_raw(example['f3'], out_type=tf.float64)
576 | ~~~
577 |
578 | 这里还用到了`tf.decode_raw`用来解析bytes数据,其输出type应该等于输入时的type,否则解析出的数据会有问题。
579 |
580 | ```python
581 | tf.decode_raw(
582 | bytes, # string类型的tensor
583 | out_type, # 输出类型,可以是`tf.half, tf.float32, tf.float64, tf.int32, tf.uint8, tf.int16, tf.int8, tf.int64`
584 | little_endian=True, # 字节顺序是否为小端序
585 | name=None)
586 | ```
587 |
588 |
589 |
590 | 无论使用哪一数据读取的方法,其过程都是一样的。过程如下:
591 |
592 | 1. 打开文件,读取数据流。
593 | 2. 将数据流解码成为指定格式。在TFRecord中,需要首先从proto数据解码成包含Example字典数据,再把其中bytes类型的数据解析成对应的张量。其它的比如csv则可以直接解析成张量。
594 | 3. 关闭文件。
595 |
596 |
597 |
598 |
599 | ## 3. 数据读取的更多方法
600 |
601 | 上文介绍了数据读取的流程以及完整读取数据的方法,但我们还可以更灵活的使用这些方法。同时,Tensorflow也提供了更多的方法来满足我们多种多样的需求。
602 |
603 |
604 |
605 | ### 3.1 多种文件读取器
606 |
607 | 上文中,我们介绍了从CSV文件中读取数据与TFRecord文件的读写数据的方式。事实中,Tensorflow除了可以读取CSV文件以及TFRecord文件以外,还可以读取二进制文件(非TFRecord文件)等多种文件。这里我们列出了Tensorflow支持的所有文件读取器。
608 |
609 | * **`tf.ReaderBase`**
610 |
611 | 所有文件读取器的基类。常用方法如下:
612 |
613 | * `tf.ReaderBase.read(queue, name=None)`:返回下一个记录 (key, value)。key表示其所读取的文件名以及行数等信息。value表示读取到的值,类型为bytes。
614 | * `tf.ReaderBase.read_up_to(queue, num_records, name=None)`:返回下num_records个记录(key, value)。其有可能返回少于num_records的记录。
615 | * `tf.ReaderBase.reset(name=None)`:将读取器恢复到初始状态。
616 |
617 | * **`tf.TextLineReader`**
618 |
619 | 文本文件读取器。可以用于读取csv、txt等文件。
620 |
621 | * **`tf.WholeFileReader`**
622 |
623 | 整个文件读取器。不同于TextLineReader读取到的数据是一行一行(以\\n换行符分割)的,WholeFileReader可以用于读取整个文件。例如读取一张图片时,我们可以使用这个方法进行读取。**仅读取一个文件时,可以使用`tf.read_file`**方法进行代替。
624 |
625 | * **`tf.IdentityReader`**
626 |
627 | 用于读取文件名。
628 |
629 | * **`tf.TFRecordReader`**
630 |
631 | 从TFRecord文件中读取数据。
632 |
633 | * **`tf.FixedLengthRecordReader`**
634 |
635 | 从文件中读取固定字节长度的数据。可以使用`tf.decode_raw`解码数据。
636 |
637 | * **`tf.LMDBReader`**
638 |
639 | 从LMDB数据库文件中读取。
640 |
641 |
642 |
--------------------------------------------------------------------------------
/ch08/ch8-dataset-02.md:
--------------------------------------------------------------------------------
1 | 在TensorFlow 1.3之后,便使用`tf.data`模块替代了原有的数据集读取以及处理方式,其操作更为简便,性能更好。此API按照功能进行划分,主要包含三部分内容:**数据读取**、**数据集与样本处理**以及**输出**。这里以一个简单的例子进行说明。
2 |
3 | ~~~python
4 | # 制作假数据样本,共5个样本
5 | inputs = tf.random_normal([5, 3])
6 | labels = tf.constant([1, 0, 1, 0, 1])
7 |
8 | # 步骤一:数据读取,生成一个dataset对象
9 | dataset = tf.data.Dataset.from_tensor_slices((inputs, labels))
10 |
11 | # 步骤二:样本处理
12 | dataset = dataset.shuffle(2) # 进行打乱,打乱时cache为2
13 | dataset = dataset.batch(3) # 设置批量大小,这里为3
14 |
15 | # 步骤三:批量输出
16 | iterator = dataset.make_initializable_iterator() # 生成迭代器对象
17 | init_op = iterator.initializer
18 | next_batch = iterator.get_next()
19 |
20 | with tf.Session() as sess:
21 | sess.run(init_op)
22 | sess.run(next_batch)
23 | ~~~
24 |
25 | 这个例子中,我们创建了5个“假样本”Tensor,并使用`tf.data.Dataset.from_tensor_slices()`方法将Tensor转化为了dataset对象。我们还可以使用`tf.data.Dataset.from_tensors()`通过内存中的张量构建dataset。更一般的,我们还可以读取磁盘上的文件,例如使用`tf.data.TFRecordDataset`读取TFRecord文件。在有了dataset之后,我们又对dataset做了一些处理,即打乱样本,并设置了批量大小。最后我们构建了迭代器对象读取数据。可以看到这里使用了`Dataset`类与`Iterator`类。
26 |
27 | ## 1. Dataset
28 |
29 | `Dataset`类包含了数据集读取、样本处理等功能。
30 |
31 | ### 1.1 数据集结构
32 |
33 | 一个数据集包含多个**元素**(此处可理解为广义上的样本),一个元素包含一个或多个Tensor对象,这些对象被称为**组件**(即样本中的特征以及标记)。`Dataset`对象包含`output_types`和`output_shapes`属性,可查看其中所有组件的推理类型与形状。例如:
34 |
35 | ~~~python
36 | dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
37 | print(dataset1.output_types) # >>>
38 | print(dataset1.output_shapes) # >>> (10,)
39 |
40 |
41 | dataset2 = tf.data.Dataset.from_tensor_slices(
42 | (tf.random_uniform([4]),
43 | tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
44 | print(dataset2.output_types) # >>> (tf.float32, tf.int32)
45 | print(dataset2.output_shapes) # >>> (TensorShape([]), TensorShape([Dimension(100)]))
46 |
47 |
48 | dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
49 | print(dataset3.output_types) # >>> (tf.float32, (tf.float32, tf.int32))
50 | print(dataset3.output_shapes) # >>> "(10, ((), (100,)))"
51 | ~~~
52 |
53 | 为每个元素的每个组件命名通常会带来便利性,例如,如果它们表示训练样本的不同特征。除元组之外,还可以使用`collections.namedtuple`或字符串映射到张量来表示`Dataset`的单个元素。例如:
54 |
55 | ~~~python
56 | dataset = tf.data.Dataset.from_tensor_slices(
57 | {"a": tf.random_uniform([4]),
58 | "b": tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)})
59 | print(dataset.output_types) # >>> "{'a': tf.float32, 'b': tf.int32}"
60 | print(dataset.output_shapes) # >>> "{'a': (), 'b': (100,)}"
61 | ~~~
62 |
63 | ### 1.2 样本处理
64 |
65 | 我们读取到的数据集往往是无法直接使用的,这时候需要对每一个样本进行处理,例如类型转化、过滤、格式转化等。这时候主要使用 `Dataset.map()`、`Dataset.flat_map()` 和 `Dataset.filter()` 等方法。这些方法可以应用于每一个元素中的每一个样本。
66 |
67 | **map**
68 |
69 | `Dataset.map()`可以对每个数据集中元素中的每个样本进行操作,用法如下:
70 |
71 | ~~~python
72 | inputs = tf.range(3)
73 | labels = tf.zeros(3)
74 |
75 | dataset = tf.data.Dataset.from_tensor_slices((inputs, labels))
76 | dataset = dataset.map(lambda x, y: (tf.not_equal(x, 0), y + 1))
77 | ~~~
78 |
79 | 这里为每一个样本输入判断是否非0,给每个label加1。
80 |
81 | `Dataset.map()`可以设置参数`num_parallel_calls`,来设置样本处理的线程数。这样在面对需要做复杂处理的数据时,可以大大提高速度。
82 |
83 | **filter**
84 |
85 | `Dataset.filter()` 可用于过滤某些样本,例如我们需要保留数据集中元素值小于5的样本,去除其它样本,我们可以进行如下操作:
86 |
87 | ~~~python
88 | inputs = tf.constant([1, 0, 6, 4, 5, 2, 7, 3])
89 | dataset = tf.data.Dataset.from_tensor_slices(inputs)
90 |
91 | # 过滤部分样本
92 | dataset = dataset.filter(lambda example: tf.less(example, 5))
93 |
94 | iterator = dataset.make_initializable_iterator()
95 | ...
96 | ~~~
97 |
98 | **skip**
99 |
100 | 使用`Dataset.skip()`方法可以跳过`Dataset`中的前n个组件,通常用于去除样本中的表头,用法如下:
101 |
102 | ```python
103 | inputs = tf.constant(['feature', '1', '2', '3', '4', '5'])
104 |
105 | dataset = tf.data.Dataset.from_tensor_slices(inputs)
106 |
107 | # 跳过第0个样本'feature',之后生成的样本从'1'开始
108 | dataset = dataset.skip(0)
109 |
110 | iterator = dataset.make_initializable_iterator()
111 | ...
112 | ```
113 |
114 | **flat_map**
115 |
116 | 有时候我们的数据集分散在多个文件中,这时候我们可以为每个文件创建一个`Dataset`对象,当我们需要对多个`Dataset`对象分别操作时,这时候可以使用`Dataset.flat_map()` ,用法如下:
117 |
118 | ```python
119 | inputs_0 = tf.constant([1, 2, 3, 4, 5])
120 | inputs_1 = tf.constant([7, 8, 9, 10, 6])
121 |
122 | dataset = tf.data.Dataset.from_tensor_slices([inputs_0, inputs_1])
123 | dataset = dataset.flat_map(
124 | lambda t: tf.data.Dataset.from_tensor_slices(t))
125 |
126 | iterator = dataset.make_initializable_iterator()
127 | ...
128 | ```
129 |
130 | 上面的例子中我们首先创建一个两个元素的数据集,然后将这个数据集的每个元素又分别作为了一个数据集, 最终的结果是使得两个1阶常量转化为了一个数据集。这个例子可能看起来是没有意义,这里我们再举一个例子。就是从两个text文件中读取数据,其每一行为一个样本,但是这两个text文件的第一行是标题,我们需要分别跳过每个文件的第一行,这时候我们可以为每个文件创建一个`Dataset`对象,然后分别操作`Dataset`对象去除第一行数据,如下:
131 |
132 | ~~~python
133 | filenames = ["/tmp/file1.csv", "/tmp/file2.csv"]
134 |
135 | # 生成文件名`Dataset`对象,其每个元素为一个文件路径
136 | dataset = tf.data.Dataset.from_tensor_slices(filenames)
137 |
138 | # 根据每个文件路径创建一个`Dataset`,并为这个`Dataset`去除第一行
139 | dataset = dataset.flat_map(
140 | lambda filename: (
141 | tf.data.TextLineDataset(filename)
142 | .skip(1)))
143 | ~~~
144 |
145 | `Dataset.flat_map()` 与`Dataset.map()`相比,最大的不同点在于`Dataset.flat_map()` 中传入的函数返回值必须是`Dataset`类型。
146 |
147 |
148 |
149 | ### 1.3 批量样本处理
150 |
151 | 除了对单个样本处理以外,我们还需要对每一个批次数据进行处理,例如打乱数据集、设置批量大小等操作。
152 |
153 | #### shuffle
154 |
155 | 使用`Dataset.shuffle()` 打乱数据集,类似于 `tf.RandomShuffleQueue` ,它保留一个固定大小的缓冲区,用于打乱样本。例如:
156 |
157 | ~~~python
158 | inputs = tf.constant([1, 2, 3, 4, 5])
159 |
160 | dataset = tf.data.Dataset.from_tensor_slices(inputs)
161 | # 打乱数据集样本,每次从3个buffer中选择样本
162 | dataset = dataset.shuffle(buffer_size=3)
163 |
164 | iterator = dataset.make_initializable_iterator()
165 | ...
166 | ~~~
167 |
168 | #### batch
169 |
170 | 使用`Dataset.batch()`设置读取数据即出队时,每次出队的样本数,用法如下:
171 |
172 | ~~~python
173 | inputs = tf.constant([1, 2, 3, 4, 5])
174 |
175 | dataset = tf.data.Dataset.from_tensor_slices(inputs_0)
176 | # 每次读取到2个样本
177 | dataset = dataset.batch(2)
178 |
179 | iterator = dataset.make_initializable_iterator()
180 | ...
181 | ~~~
182 |
183 | **注意**:通常的,我们首先打乱数据,然后在生成批次样本,如果顺序颠倒,则会出现一个批次内样本没有打乱的情况。
184 |
185 | 使用batch时,要求batch中每一个元素与其它元素的每一个组件都拥有相同的`shape`,此时才能合成一个batch。当batch中不同元素的组件`shape`不相同时,我们也可以使用`padded_batch`对组件进行填充,使其`shape`相同,用法如下:
186 |
187 | ~~~python
188 | dataset = tf.data.Dataset.range(100)
189 | dataset = dataset.map(lambda x: tf.fill([tf.cast(x, tf.int32)], x))
190 | dataset = dataset.padded_batch(4, padded_shapes=[None])
191 |
192 | iterator = dataset.make_one_shot_iterator()
193 | next_element = iterator.get_next()
194 |
195 | print(sess.run(next_element)) # ==> [[0, 0, 0], [1, 0, 0], [2, 2, 0], [3, 3, 3]]
196 | print(sess.run(next_element)) # ==> [[4, 4, 4, 4, 0, 0, 0],
197 | # [5, 5, 5, 5, 5, 0, 0],
198 | # [6, 6, 6, 6, 6, 6, 0],
199 | # [7, 7, 7, 7, 7, 7, 7]]
200 | ~~~
201 |
202 | #### repeat
203 |
204 | 使用`Dataset.repeat()`方法通常用来设置样本重复使用代数,默认的所有样本只能使用1一次,即默认为1代,当所有元素均出队完成再去获取元素时,会出现`OutOfRangeError`的错误,当设置为`None`时表示不限制样本使用次数。如下:
205 |
206 | ~~~python
207 | inputs = tf.constant([1, 2, 3, 4, 5])
208 |
209 | dataset = tf.data.Dataset.from_tensor_slices(inputs_0)
210 | # 不限制代数
211 | dataset = dataset.repeat(None)
212 |
213 | iterator = dataset.make_initializable_iterator()
214 | ...
215 | ~~~
216 |
217 | ### 1.4 其它操作
218 |
219 | ~~~python
220 | # 预取操作,类似于缓存,一般用在map或batch之后
221 | tf.data.Dataset.prefetch()
222 |
223 | # 将dataset缓存在内存或硬盘上,默认的不指定路径即为内存(慎用)
224 | tf.data.Dataset.cache()
225 | ~~~
226 |
227 |
228 |
229 | ## 2. Iterator
230 |
231 | 创建了表示输入数据的`Dataset`之后,下一步需要创建`Iterator`来访问数据集中的元素。迭代器的种类有很多种,由简单到复杂的迭代器如下:
232 |
233 | * 单次迭代器
234 | * 可初始化迭代器
235 | * 可重新初始化迭代器
236 | * 可馈送迭代器
237 |
238 | ### 2.1 单次Iterator
239 |
240 | 单次迭代器是最近单的迭代器形式,用法简单,如下:
241 |
242 | ~~~python
243 | dataset = tf.data.Dataset.range(100)
244 |
245 | iterator = dataset.make_one_shot_iterator()
246 | next_element = iterator.get_next()
247 |
248 | for i in range(100):
249 | value = sess.run(next_element)
250 | assert i == value
251 | ~~~
252 |
253 | 这里,我们使用`Dataset`对象的`make_one_shot_iterator()`方法创建了单次`Iterator`对象,然后调用其`get_next()`方法即可获得数据,循环调用时,每次都会出队一个数据。
254 |
255 | **注意**:单次迭代器是目前唯一可轻松与`Estimator`配合使用的类型。
256 |
257 | ### 2.2 可初始化Iterator
258 |
259 | 单次迭代器用法简便,但这也限制其部分功能的使用,例如有时候无法使用占位符,如果要使用占位符,那么必须使用可初始化的迭代器。用法如下:
260 |
261 | ~~~python
262 | max_value = tf.placeholder(tf.int64, shape=[])
263 | dataset = tf.data.Dataset.range(max_value)
264 | iterator = dataset.make_initializable_iterator()
265 | next_element = iterator.get_next()
266 |
267 | sess.run(iterator.initializer, feed_dict={max_value: 10})
268 | for i in range(10):
269 | value = sess.run(next_element)
270 | assert i == value
271 | ~~~
272 |
273 | 可以看到可初始化迭代器可以方便的加入占位符,但相应的,我们必须对迭代器进行显式的初始化,然后在初始化时把占位符使用具体的张量进行替代。
274 |
275 | 事实上,可初始化迭代器还可以进行多次初始化,用法如下:
276 |
277 | ~~~python
278 | max_value = tf.placeholder(tf.int64, shape=[])
279 | dataset = tf.data.Dataset.range(max_value)
280 | iterator = dataset.make_initializable_iterator()
281 | next_element = iterator.get_next()
282 |
283 | # 第一次初始化,max_value使用10替代
284 | sess.run(iterator.initializer, feed_dict={max_value: 10})
285 | for i in range(10):
286 | value = sess.run(next_element)
287 | assert i == value
288 |
289 | # 第二次初始化,max_value使用100替代
290 | sess.run(iterator.initializer, feed_dict={max_value: 100})
291 | for i in range(100):
292 | value = sess.run(next_element)
293 | assert i == value
294 | ~~~
295 |
296 | ### 2.3 可重新初始化迭代器
297 |
298 | 有时候,我们需要构建多个数据集对象,而且这些数据集对象都是相似的。例如我们在训练模型时往往既需要构建训练输入管道,又需要构建验证输入管道,这时候可以构建可重新初始化迭代器。例如:
299 |
300 | ~~~python
301 | # 构建结构类似(组件shape与dtype相同)的训练集与验证集dataset对象
302 | training_dataset = tf.data.Dataset.range(100).map(
303 | lambda x: x + tf.random_uniform([], -10, 10, tf.int64))
304 | validation_dataset = tf.data.Dataset.range(50)
305 |
306 | # 从一个“结构”中创建迭代器,这里使用的是训练集的dtypes与shapes,验证集的也可以。
307 | iterator = tf.data.Iterator.from_structure(training_dataset.output_types,
308 | training_dataset.output_shapes)
309 | next_element = iterator.get_next()
310 |
311 | # 训练集与验证集的初始化op
312 | training_init_op = iterator.make_initializer(training_dataset)
313 | validation_init_op = iterator.make_initializer(validation_dataset)
314 |
315 | with tf.Session() as sess:
316 | # 运行20代,每代首先出队100个训练集可用于训练,然后在出队50个验证集,可用于验证
317 | for _ in range(20):
318 | sess.run(training_init_op)
319 | for _ in range(100):
320 | sess.run(next_element)
321 |
322 | sess.run(validation_init_op)
323 | for _ in range(50):
324 | sess.run(next_element)
325 | ~~~
326 |
327 | **注意**:使用可重新初始化迭代器时,每次初始化迭代器之后,都会重新从数据集的开头遍历数据,这在有时候是不可取。
328 |
329 | ### 2.4 可馈送迭代器
330 |
331 | 为了避免上述可重新初始化迭代器每次初始化之后从头遍历数据的情况,我们可以使用可馈送迭代器,其功能类似于可重新初始化迭代器。用法如下:
332 |
333 | ~~~python
334 | # 构建结构类似(组件shape与dtype相同)的训练集与验证集dataset对象
335 | training_dataset = tf.data.Dataset.range(100).map(
336 | lambda x: x + tf.random_uniform([], -10, 10, tf.int64)).repeat()
337 | validation_dataset = tf.data.Dataset.range(50)
338 |
339 | # 获取训练集与验证集的对应的迭代器
340 | training_iterator = training_dataset.make_one_shot_iterator()
341 | validation_iterator = validation_dataset.make_initializable_iterator()
342 |
343 | # 构建可馈送迭代器,这里使用`placeholder`构建,后面我们可使用训练集与验证集的
344 | # handle替换此处的handle,从而切换数据集对应迭代器
345 | handle = tf.placeholder(tf.string, shape=[])
346 | iterator = tf.data.Iterator.from_string_handle(
347 | handle, training_dataset.output_types, training_dataset.output_shapes)
348 | next_element = iterator.get_next()
349 |
350 | with tf.Session() as sess:
351 | # 获得训练集与验证集的handle
352 | training_handle = sess.run(training_iterator.string_handle())
353 | validation_handle = sess.run(validation_iterator.string_handle())
354 |
355 | for _ in range(20):
356 | for _ in range(100):
357 | sess.run(next_element, feed_dict={handle: training_handle})
358 |
359 | sess.run(validation_iterator.initializer)
360 | for _ in range(50):
361 | sess.run(next_element, feed_dict={handle: validation_handle})
362 | ~~~
363 |
364 | 上面的例子中,每一代的训练集出队时都是从上次出队的位置继续出队,可以保证训练集样本是连续输出的,而验证集每次都从头获取,这是我们常见的训练集与验证集的用法。
365 |
366 | **注意:**由于`tf.placeholder`不支持TensorFlow中的`Tensor`传入,所以必须在获取数据集之前首先在会话中得到训练集与验证集的`handle`。
367 |
368 | 事实上,我们除了借助`tf.placeholder`切换`handle`以外,也可以使用变量进行切换,如下:
369 |
370 | ~~~python
371 | # 构建结构类似(组件shape与dtype相同)的训练集与验证集dataset对象
372 | training_dataset = tf.data.Dataset.range(100).map(
373 | lambda x: x + tf.random_uniform([], -10, 10, tf.int64)).repeat()
374 | validation_dataset = tf.data.Dataset.range(50)
375 |
376 | # 获取训练集与验证集的对应的迭代器
377 | training_iterator = training_dataset.make_one_shot_iterator()
378 | validation_iterator = validation_dataset.make_initializable_iterator()
379 |
380 | # 构建可馈送迭代器,这里使用`Variable`构建,后面我们可使用训练集与验证集的
381 | # handle张量赋值给此处的handle变量,从而切换数据集对应迭代器
382 | handle = tf.Variable('')
383 | iterator = tf.data.Iterator.from_string_handle(
384 | handle, training_dataset.output_types, training_dataset.output_shapes)
385 | next_element = iterator.get_next()
386 |
387 | with tf.Session() as sess:
388 | for _ in range(20):
389 | # handle赋值为训练集的handle
390 | sess.run(handle.assign(training_iterator.string_handle()))
391 | for _ in range(100):
392 | sess.run(next_element)
393 |
394 | sess.run(validation_iterator.initializer)
395 | # handle赋值为验证集的handle
396 | sess.run(handle.assign(validation_iterator.string_handle()))
397 | for _ in range(50):
398 | sess.run(next_element)
399 | ~~~
400 |
401 | 上述的两种用法都是等价的。可以看到使用可馈送迭代器可以方便灵活的切换我们想要使用的数据集。
402 |
403 | ## 3. 高效读取数据
404 |
405 | 直接从原始文件中读取数据可能存在问题,例如当文件数量较多,且解码不够迅速时,就会影响模型训练速度。在TensorFlow中,我们可以使用TFRecord对数据进行存取。TFRecord是一种二进制文件。可以更快速的操作文件。
406 |
407 | 通常我们得到的数据集并不是TFRecord格式,例如MNIST数据集也是一个二进制文件,每一个字节都代表一个像素值(除去开始的几个字节外)或标记,这与TFRecord文件的数据表示方法(TFRecord的二进制数据中还包含了校验值等数据)并不一样。所以,有时候我们需要将数据转化为TFRecord文件。这里需要注意并不是每一个数据集均需要转化为TFRecord文件,建议将文件数量较多,直接读取效率低下的数据集转化为TFRecord文件格式。
408 |
409 | ### 3.1 写入TFRecord文件
410 |
411 | TFRecord文件的存取,本质上是对生成的包含样本数据的ProtoBuf数据的存取,TFRecord文件只适合用来存储数据样本。一个TFRecord文件存储了一个或多个example对象,example.proto文件描述了一个样本数据遵循的格式。每个样本example包含了多个特征feature,feature.proto文件描述了特征数据遵循的格式。
412 |
413 | 在了解如何写入TFRecord文件前,我们首先了解一下其对应的消息定义文件。通过这个文件,我们可以知道消息的格式。
414 |
415 | **feature.proto**的内容如下(删除了大部分注释内容):
416 |
417 | ```protobuf
418 | syntax = "proto3";
419 | option cc_enable_arenas = true;
420 | option java_outer_classname = "FeatureProtos";
421 | option java_multiple_files = true;
422 | option java_package = "org.tensorflow.example";
423 |
424 | package tensorflow;
425 |
426 | // Containers to hold repeated fundamental values.
427 | message BytesList {
428 | repeated bytes value = 1;
429 | }
430 | message FloatList {
431 | repeated float value = 1 [packed = true];
432 | }
433 | message Int64List {
434 | repeated int64 value = 1 [packed = true];
435 | }
436 |
437 | // Containers for non-sequential data.
438 | message Feature {
439 | // Each feature can be exactly one kind.
440 | oneof kind {
441 | BytesList bytes_list = 1;
442 | FloatList float_list = 2;
443 | Int64List int64_list = 3;
444 | }
445 | };
446 |
447 | message Features {
448 | // Map from feature name to feature.
449 | map feature = 1;
450 | };
451 |
452 | message FeatureList {
453 | repeated Feature feature = 1;
454 | };
455 |
456 | message FeatureLists {
457 | // Map from feature name to feature list.
458 | map feature_list = 1;
459 | };
460 | ```
461 |
462 | 可以看到一个特征`Feature`可以是3中数据类型(`BytesList`,`FloatList`,`Int64List`)之一。多个特征`Feature`组成一个组合特征`Features`,多个特征列表组成特征列表组`FeatureLists`。
463 |
464 | **example.proto**的内容如下(删除了大部分注释内容):
465 |
466 | ```protobuf
467 | syntax = "proto3";
468 |
469 | import "tensorflow/core/example/feature.proto";
470 | option cc_enable_arenas = true;
471 | option java_outer_classname = "ExampleProtos";
472 | option java_multiple_files = true;
473 | option java_package = "org.tensorflow.example";
474 |
475 | package tensorflow;
476 |
477 | message Example {
478 | Features features = 1;
479 | };
480 |
481 | message SequenceExample {
482 | Features context = 1;
483 | FeatureLists feature_lists = 2;
484 | };
485 | ```
486 |
487 | 可以看到一个样本`Example`包含一个特征组合。序列样本`SequenceExample`包含一个类型是特征组合的上下文`context`与一个特征列表组`feature_lists`。
488 |
489 | 可以看到:**TFRecord存储的样本数据是以样本为单位的。**
490 |
491 | 了解了TFRecord读写样本的数据结构之后,我们就可以使用相关API进行操作。
492 |
493 | #### 写入数据
494 |
495 | TensorFlow已经为我们封装好了操作protobuf的方法以及文件写入的方法。写入数据的第一步是打开文件并创建writer对象,Tensorflow使用`tf.python_io.TFRecordWriter`来完成,具体如下:
496 |
497 | ```python
498 | # 传入一个路径,返回一个writer上下文管理器
499 | tf.python_io.TFRecordWriter(path, options=None)
500 | ```
501 |
502 | TFRecordWriter拥有`write`,`flush`,`close`方法,分别用于写入数据到缓冲区,将缓冲区数据写入文件并清空缓冲区,关闭文件流。
503 |
504 | 开启文件之后,需要创建样本对象并将example数据写入。根据上面的proto中定义的数据结构,我们知道一个样本对象包含多个特征。所以我们首先需要创建特征对象,然后再创建样本对象。如下为序列化一个样本的例子:
505 |
506 | ```python
507 | with tf.python_io.TFRecordWriter('./test.tfrecord') as writer:
508 | f1 = tf.train.Feature(int64_list=tf.train.Int64List(value=[i]))
509 | f2 = tf.train.Feature(float_list=tf.train.FloatList(value=[1. , 2.]))
510 | b = np.ones([i]).tobytes() # 此处默认为float64类型
511 | f3 = tf.train.Feature(bytes_list=tf.train.BytesList(value=[b]))
512 |
513 | features = tf.train.Features(feature={'f1': f1, 'f2': f2, 'f3': f3})
514 | example = tf.train.Example(features=features)
515 |
516 | writer.write(example.SerializeToString())
517 | ```
518 |
519 | 序列化多个样本只需要重复上述的写入过程即可。如下:
520 |
521 | ```python
522 | with tf.python_io.TFRecordWriter('./test.tfrecord') as writer:
523 | # 多个样本多次写入
524 | for i in range(1, 6):
525 | f1 = tf.train.Feature(int64_list=tf.train.Int64List(value=[i]))
526 | f2 = tf.train.Feature(float_list=tf.train.FloatList(value=[1. , 2.]))
527 | b = np.ones([i]).tobytes()
528 | f3 = tf.train.Feature(bytes_list=tf.train.BytesList(value=[b]))
529 |
530 | features = tf.train.Features(feature={'f1': f1, 'f2': f2, 'f3': f3})
531 | example = tf.train.Example(features=features)
532 |
533 | writer.write(example.SerializeToString())
534 | ```
535 |
536 | **注意事项:**
537 |
538 | - `tf.train.Int64List`、`tf.train.FloatList`、`tf.train.BytesList`均要求输入的是python中的list类型的数据,而且list中的元素分别只能是int、float、bytes这三种类型。
539 | - 由于生成protobuf数据对象的类中,只接受关键字参数,所以参数必须写出参数名。
540 | - protobuf数据对象类需要遵循proto文件中定义的数据结构来使用。
541 |
542 | TFRecord文件的数据写入是在Python环境中完成的,不需要启动会话。写入数据的过程可以看做是原结构数据转换为python数据结构,再转换为proto数据结构的过程。完整的数据写入过程如下:
543 |
544 | 1. 读取文件中的数据。
545 | 2. 组织读取到的数据,使其成为以“样本”为单位的数据结构。
546 | 3. 将“样本”数据转化为Python中的数据结构格式(int64\_list,float\_list,bytes\_list三种之一)。
547 | 4. 将转化后的数据按照proto文件定义的格式写出“Example”对象。
548 | 5. 将“Example”对象中存储的数据序列化成为二进制的数据。
549 | 6. 将二进制数据存储在TFRecord文件中。
550 |
551 | ### 3.2 读取TFRecord文件
552 |
553 | 读取TFRecord文件类似于读取csv文件。只不过使用的是`tf.TFRecordReader`进行读取。除此以外,还需要对读取到的数据进行解码。`tf.TFRecordReader`用法如下:
554 |
555 | ```python
556 | reader = tf.TFRecordReader(name=None, options=None)
557 | key, value = reader.read(queue, name=None)
558 | ```
559 |
560 | 此处读取到的value是序列化之后的proto样本数据,我们还需要对数据进行解析,这里可以使用方法`tf.parse_single_example`。解析同样需要说明解析的数据格式,这里可以使用`tf.FixedLenFeature`与`tf.VarLenFeature`进行描述。
561 |
562 | 解析单个样本的方法如下:
563 |
564 | ```python
565 | # 解析单个样本
566 | tf.parse_single_example(serialized, features, name=None, example_names=None)
567 | ```
568 |
569 | 解析设置数据格式的方法如下
570 |
571 | ```python
572 | # 定长样本
573 | tf.FixedLenFeature(shape, dtype, default_value=None)
574 | # 不定长样本
575 | tf.VarLenFeature(dtype)
576 | ```
577 |
578 | 完整例子如下(解析的是上文中写入TFRecord文件的例子中生成的文件):
579 |
580 | ```python
581 | reader = tf.TFRecordReader()
582 | key, value = reader.read(filename_queue)
583 |
584 | example = tf.parse_single_example(value, features={
585 | 'f1': tf.FixedLenFeature([], tf.int64),
586 | 'f2': tf.FixedLenFeature([2], tf.float32),
587 | 'f3': tf.FixedLenFeature([], tf.string)})
588 |
589 | feature_1 = example['f1']
590 | feature_2 = example['f2']
591 | feature_3 = tf.io.decode_raw(example['f3'], out_type=tf.float64)
592 | ```
593 |
594 | 这里还用到了`tf.io.decode_raw`用来解析bytes数据,其输出type应该等于输入时的type,否则解析出的数据会有问题。
595 |
596 | ```python
597 | tf.io.decode_raw(
598 | bytes, # string类型的tensor
599 | out_type, # 输出类型,可以是`tf.half, tf.float32, tf.float64, tf.int32, tf.uint8, tf.int16, tf.int8, tf.int64`
600 | little_endian=True, # 字节顺序是否为小端序
601 | name=None)
602 | ```
603 |
604 | 无论使用哪一数据读取的方法,其过程都是一样的。过程如下:
605 |
606 | 1. 打开文件,读取数据流。
607 | 2. 将数据流解码成为指定格式。在TFRecord中,需要首先从proto数据解码成包含Example字典数据,再把其中bytes类型的数据解析成对应的张量。其它的比如csv则可以直接解析成张量。
608 | 3. 关闭文件。
609 |
610 | ## 4. data模块下的多种数据集读取
611 |
612 | 除了上述的从内存中的张量中读取数据集以外,还可以从文本、TFRecord文件等不同数据集文件中读取数据集。
613 |
614 | **从内存中的张量中读取数据:**
615 |
616 | ~~~python
617 | features = np.random.normal(size=[100, 3])
618 | labels = np.random.binomial(1, .5, [100])
619 | dataset = tf.data.Dataset.from_tensor_slices((features, labels))
620 | ~~~
621 |
622 | **从文本文件中读取数据:**
623 |
624 | ~~~python
625 | filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]
626 | dataset = tf.data.TextLineDataset(filenames)
627 | ~~~
628 |
629 | 这时候每一行都会作为一个样本,此时dataset中只有一个元素,元素的类型为`tf.string`。这样的数据往往并不能直接使用,还需进行处理,可以使用`map`等方法逐元素处理。
630 |
631 | **从TFRecord文件中读取数据:**
632 |
633 | ~~~python
634 | filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
635 | dataset = tf.data.TFRecordDataset(filenames)
636 | ~~~
637 |
638 | 从TFRecord文件中读取到的数据也需要使用`map`等方法对每条数据进行解析。例如:
639 |
640 | ~~~python
641 | # 将string类型的`example_proto`转化为一个string类型组件和int类型组件,分别代表图片与标记
642 | def _parse_function(example_proto):
643 | features = {"image": tf.FixedLenFeature((), tf.string, default_value=""),
644 | "label": tf.FixedLenFeature((), tf.int32, default_value=0)}
645 | parsed_features = tf.parse_single_example(example_proto, features)
646 | return parsed_features["image"], parsed_features["label"]
647 |
648 | filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
649 | dataset = tf.data.TFRecordDataset(filenames)
650 | dataset = dataset.map(_parse_function)
651 | ~~~
652 |
653 |
--------------------------------------------------------------------------------
/ch09/ch9-saver.md:
--------------------------------------------------------------------------------
1 | ## 1. 图存取
2 |
3 | 图是算法过程的描述工具,当我们在某个文件中定义了一个图的时候,也就定义了一个算法,当我们需要运行这个算法时,可以直接找到定义此图的Python文件,就能操作它。但为了方便,我们也可以将图序列化。
4 |
5 | 图是由一系列Op与Tensor构成的,我们可以通过某种方法对这些Op与Tensor进行描述,在Tensorflow中这就是'图定义'`GraphDef`。图的存取本质上就是`GraphDef`的存取。
6 |
7 | ### 1.1 图的保存
8 |
9 | 图的保存方法很简单,只需要将图的定义保存即可。所以:
10 |
11 | **第一步,需要获取图定义。**
12 |
13 | 可以使用`tf.Graph.as_graph_def`方法来获取序列化后的图定义`GraphDef`。
14 |
15 | 例如:
16 |
17 | ```python
18 | with tf.Graph().as_default() as graph:
19 | v = tf.constant([1, 2])
20 | print(graph.as_graph_def())
21 | ```
22 |
23 | 输出内容:
24 |
25 | ```python
26 | 输入内容:
27 | node {
28 | name: "Const"
29 | op: "Const"
30 | attr {
31 | key: "dtype"
32 | value {
33 | type: DT_INT32
34 | }
35 | }
36 | attr {
37 | key: "value"
38 | value {
39 | tensor {
40 | dtype: DT_INT32
41 | tensor_shape {
42 | dim {
43 | size: 2
44 | }
45 | }
46 | tensor_content: "\001\000\000\000\002\000\000\000"
47 | }
48 | }
49 | }
50 | }
51 | versions {
52 | producer: 24
53 | }
54 | ```
55 |
56 | 还可以使用绑定图的会话的`graph_def`属性来获取图的序列化后的定义,例如:
57 |
58 | ```python
59 | with tf.Graph().as_default() as graph:
60 | v = tf.constant([1, 2])
61 | print(graph.as_graph_def())
62 |
63 | with tf.Session(graph=graph) as sess:
64 | sess.graph_def == graph.as_graph_def() # True
65 | ```
66 |
67 | **注意:**当会话中加入Op时,`sess.graph_def == graph.as_graph_def()`不再成立。在会话中graph_def会随着Op的改变而改变。
68 |
69 | 获取了图的定义之后,便可以去保存图。
70 |
71 | **第二步:保存图的定义**
72 |
73 | 保存图的定义有两种方法,第一种为直接将图存为文本文件。第二种为使用Tensorflow提供的专门的保存图的方法,这种方法更加便捷。
74 |
75 | - 方法一:
76 |
77 | 直接创建一个文件保存图定义。如下:
78 |
79 | ```python
80 | with tf.Graph().as_default() as g:
81 | tf.Variable([1, 2], name='var')
82 |
83 | with tf.gfile.FastGFile('test_model.pb', 'wb') as f:
84 | f.write(g.as_graph_def().SerializeToString())
85 | ```
86 |
87 | `SerializeToString`是将str类型的图定义转化为二进制的proto数据。
88 |
89 | - 方法二:
90 |
91 | 使用Tensorflow提供的`tf.train.write_graph`进行保存。使用此方法还有一个好处,就是可以直接将图传入即可。用法如下:
92 |
93 | ```python
94 | tf.train.write_graph(
95 | graph_or_graph_def, # 图或者图定义
96 | logdir, # 存储的文件路径
97 | name, # 存储的文件名
98 | as_text=True) # 是否作为文本存储
99 | ```
100 |
101 | 例如,'方法一'中的图也可以这样保存:
102 |
103 | ```python
104 | with tf.Graph().as_default() as g:
105 | tf.Variable([1, 2], name='var')
106 | tf.train.write_graph(g, '', 'test_model.pb', as_text=False)
107 | ```
108 |
109 | 这些参数`as_text`的值为`False`,即保存为二进制的proto数据。此方法等价于'方法一'。
110 |
111 | 当`as_text`值为`True`时,保存的是str类型的数据。通常推荐为`False`。
112 |
113 | ### 1.2 图的读取
114 |
115 | 图的读取,即将保存的图的节点加载到当前的图中。当我们保存一个图之后,这个图可以再次被获取到。
116 |
117 | 图的获取步骤如下:
118 |
119 | 1. 从序列化的二进制文件中读取数据
120 | 2. 从读取到数据中创建`GraphDef`对象
121 | 3. 导入`GraphDef`对象到当前图中,创建出对应的图结构
122 |
123 | 具体如下:
124 |
125 | ```python
126 | with tf.Graph().as_default() as new_graph:
127 | with tf.gfile.FastGFile('test_model.pb', 'rb') as f:
128 | graph_def = tf.GraphDef()
129 | graph_def.ParseFromString(f.read())
130 | tf.import_graph_def(graph_def)
131 | ```
132 |
133 | 这里`ParseFromString`是protocal message的方法,用于将二进制的proto数据读取成`GraphDef`数据。`tf.import_graph_def`用于将一个图定义导入到当前的默认图中。
134 |
135 | 这里有一个问题,那就是导入图中的Op与Tensor如何获取到呢?`tf.import_graph_def`都已经帮我们想到这些问题了。这里,我们可以了解下`tf.import_graph_def`的用法:
136 |
137 | ```python
138 | tf.import_graph_def(
139 | graph_def, # 将要导入的图的图定义
140 | input_map=None, # 替代导入的图中的Tensor
141 | return_elements=None, # 返回指定的OP或Tensor(可以使用新的变量绑定)
142 | name=None, # 被导入的图中Op与Tensor的name前缀 默认是'import'
143 | op_dict=None,
144 | producer_op_list=None):
145 | ```
146 |
147 | **注意**:当`input_map`不为None时,`name`必须不为空。
148 |
149 | **注意**:当`return_elements`返回Op时,在会话中执行返回为`None`。
150 |
151 | 当然了,我们也可以使用`tf.Graph.get_tensor_by_name`与`tf.Graph.get_operation_by_name`来获取Tensor与Op,但要**注意加上name前缀**。
152 |
153 | 如果图在保存时,存为文本类型的proto数据,即`tf.train.write_graph`中的参数`as_text`为`True`时,获取图的操作稍有不同。即解码时不能使用`graph_def.ParseFromString`进行解码,而需要使用`protobuf`中的`text_format`进行操作,如下:
154 |
155 | ```python
156 | from google.protobuf import text_format
157 |
158 | with tf.Graph().as_default() as new_graph:
159 | with tf.gfile.FastGFile('test_model.pb', 'r') as f:
160 | graph_def = tf.GraphDef()
161 | # graph_def.ParseFromString(f.read())
162 | text_format.Merge(f.read(), graph_def)
163 | tf.import_graph_def(graph_def)
164 | ```
165 |
166 | ## 2. 变量存取
167 |
168 | 变量存储是把模型中定义的变量存储起来,不包含图结构。另一个程序使用时,首先需要重新创建图,然后将存储的变量导入进来,即模型加载。变量存储可以脱离图存储而存在。
169 |
170 | 变量的存储与读取,在Tensorflow中叫做检查点存取,变量保存的文件是检查点文件(checkpoint file),扩展名一般为.ckpt。使用`tf.train.Saver()`类来操作检查点。
171 |
172 | ### 2.1 变量存储
173 |
174 | 变量是在图中定义的,但实际上是会话中存储了变量,即我们在运行图的时候,变量才会真正存在,且变量在图的运行过程中,值会发生变化,所以我们需要**在会话中保存变量**。保存变量的方法是`tf.train.Saver.save()`。
175 |
176 | 这里需要注意,通常,我们可以在图定义完成之后初始化`tf.train.Saver()`。`tf.train.Saver()`在图中的位置很重要,在其之后的变量不会被存储在当前的`Save`对象控制。
177 |
178 | 创建Saver对象之后,此时并不会保存变量,我们还需要指定会话运行到什么时候时再去保存变量,需要使用`tf.train.Saver.save()`进行保存。
179 |
180 | 例如:
181 |
182 | ```python
183 | with tf.Graph().as_default() as g:
184 | var1 = tf.Variable(1)
185 | saver = tf.train.Saver()
186 |
187 | with tf.Session(graph=g) as sess:
188 | sess.run(tf.global_variables_initializer())
189 | sess.run(var1.assign_add(1))
190 | saver.save(sess, './model.cpkt')
191 | ```
192 |
193 | `tf.train.Saver.save()`有两个必填参数,第一个是会话,第二个是存储路径。执行保存变量之后,会在指定目录下生成四个文件。分别是checkpoint文件、model.ckpt.data-00000-of-00001文件、model.ckpt.index文件、model.ckpt.meta文件。这四个文件的作用分别是:
194 |
195 | - checkpoint:为文本文件。记录当前模型最近的5次变量保存的路径信息。这里我们只保存了一个模型,所有只有一次模型保存信息,也是当前模型的信息。
196 | - model.ckpt.data-00000-of-00001:保存了当前模型变量的数据。
197 | - model.ckpt.meta:保存了`MetaGraphDef`,可以用于恢复saver对象。
198 | - model.ckpt.index:辅助model.ckpt.meta的数据文件。
199 |
200 | #### 循环迭代算法时存储变量
201 |
202 | 实际中,我们训练一个模型,通常需要迭代较多的次数,迭代的过程会用去很多的时间,为了避免出现意外情况(例如断电、死机),我们可以每迭代一定次数,就保存一次模型,如果出现了意外情况,就可以快速恢复模型,不至于重新训练。
203 |
204 | 如下,我们需要迭代1000次模型,每100次迭代保存一次:
205 |
206 | ```python
207 | with tf.Graph().as_default() as g:
208 | var1 = tf.Variable(1, name='var')
209 | saver = tf.train.Saver()
210 |
211 | with tf.Session(graph=g) as sess:
212 | sess.run(tf.global_variables_initializer())
213 |
214 | for i in range(1, 1001):
215 | sess.run(var1.assign_add(1))
216 | if i % 100 == 0:
217 | saver.save(sess, './model2.cpkt')
218 |
219 | saver.save(sess, './model2.cpkt')
220 | ```
221 |
222 | 这时候每次存储都会覆盖上次的存储信息。但我们存储的模型并没有与训练的次数关联起来,我们并不知道当前存储的模型是第几次训练后保存的结果,如果中途出现了意外,我们并不知道当前保存的模型是什么时候保存下的。所以通常的,我们还需要将训练的迭代的步数进行标注。在Tensorflow中只需要给save方法加一个参数即可,如下:
223 |
224 | ```python
225 | with tf.Graph().as_default() as g:
226 | var1 = tf.Variable(1, name='var')
227 | saver = tf.train.Saver()
228 |
229 | with tf.Session(graph=g) as sess:
230 | sess.run(tf.global_variables_initializer())
231 |
232 | for i in range(1, 1001):
233 | sess.run(var1.assign_add(1))
234 | if i % 100 == 0:
235 | saver.save(sess, './model2.cpkt', global_step=i) #
236 |
237 | saver.save(sess, './model2.cpkt', 1000)
238 | ```
239 |
240 | 这里,我们增加了给`saver.save`增加了`global_step`的参数,这个参数可以是一个0阶Tensor或者一个整数。之后,我们生成的保存变量文件的文件名会加上训练次数,并同时保存最近5次的训练结果,即产生了16个文件。包括这五次结果中每次训练结果对应的data、meta、index三个文件,与一个checkpoint文件。
241 |
242 | #### 检查保存的变量信息
243 |
244 | TensorFlow提供了很多种方法查看检查点文件中张量,例如可以使用`tf.train.NewCheckpointReader`。用法如下:
245 |
246 | ~~~python
247 | reader = tf.train.NewCheckpointReader('./model.ckpt')
248 |
249 | tensor = reader.get_tensor('TensorName')
250 | ~~~
251 |
252 | 其中,使用`has_tensor`可以查看是否存在某个张量,使用`get_tensor`可以获取某个张量
253 |
254 | 或者也可以利用包`inspect_checkpoint`中的方法对检查点文件操作,如下:
255 |
256 | ~~~python
257 | # 导入 inspect_checkpoint 库
258 | from tensorflow.python.tools import inspect_checkpoint as chkp
259 |
260 | # 打印checkpoint文件中所有的tensor
261 | chkp.print_tensors_in_checkpoint_file("./model.ckpt", tensor_name='', all_tensors=True)
262 |
263 | # tensor_name: v1
264 | # [ 1. 1. 1.]
265 | # tensor_name: v2
266 | # [-1. -1. -1. -1. -1.]
267 |
268 | # 打印checkpoint文件中,tensor_name为v1的张量
269 | chkp.print_tensors_in_checkpoint_file("/tmp/model.ckpt", tensor_name='v1', all_tensors=False)
270 |
271 | # tensor_name: v1
272 | # [ 1. 1. 1.]
273 | ~~~
274 |
275 | ### 2.2 变量读取
276 |
277 | 变量读取,即加载模型数据,为了保证数据能够正确加载,必须首先将图定义好,而且必须与保存时的图定义一致。这里“一致”的意思是图相同,对于Python句柄等Python环境中的内容可以不同。
278 |
279 | 下面我们恢复上文中保存的图:
280 |
281 | ```python
282 | with tf.Graph().as_default() as g:
283 | var1 = tf.Variable(1, name='var')
284 | saver = tf.train.Saver()
285 |
286 | with tf.Session(graph=g) as sess:
287 | saver.restore(sess, './model.ckpt')
288 | print(sess.run(var)) # >> 1001
289 | ```
290 |
291 | **注意**:当使用restore方法恢复变量时,可以不用初始化方法初始化变量,但这仅仅在所有变量均会被恢复时可用,当存在部分变量没有恢复时,必须使用变量初始化方法进行初始化。
292 |
293 | 除了使用这种方法恢复变量以外,还可以借助meta graph,例如:
294 |
295 | ```python
296 | with tf.Graph().as_default() as g:
297 | var1 = tf.Variable(1, name='var')
298 |
299 | with tf.Session(graph=g) as sess:
300 | sess.run(tf.global_variables_initializer())
301 | saver = tf.train.import_meta_graph('./log/model.ckpt.meta')
302 | saver.restore(sess, './model.ckpt')
303 | print(sess.run(var)) # >> 1001
304 | ```
305 |
306 | 可以看到meta graph包含了saver对象的信息,不仅如此,使用meta数据进行恢复时,meta数据中也包含了图定义。所以在我们没有图定义的情况下,也可以使用meta数据进行恢复,例如:
307 |
308 | ```python
309 | with tf.Graph().as_default() as g:
310 | op = tf.no_op()
311 |
312 | with tf.Session(graph=g) as sess:
313 | sess.run(tf.global_variables_initializer())
314 | saver = tf.train.import_meta_graph('./log/model.ckpt.meta')
315 | saver.restore(sess, './model.ckpt')
316 | print(sess.run(g.get_tensor_by_name('var:0'))) # >> 1001
317 | ```
318 |
319 | 所以,通过meta数据不仅能够恢复变量,也能够恢复图定义。
320 |
321 | 训练模型时,往往每过一段时间就会保存一次模型,这时候checkpoint文件会记录最近五次的ckpt文件名记录,所以在恢复数据时,当存在多个模型保存的文件,为了简便可以使用`tf.train.get_checkpoint_state()`来读取checkpoint文件并获取最新训练的模型。其返回一个`CheckpointState`对象,可以使用这个对象`model_checkpoint_path`属性来获得最新模型的路径。例如:
322 |
323 | ```python
324 | ...
325 | with tf.Session() as sess:
326 | sess.run(tf.global_variables_initializer())
327 |
328 | ckpt = tf.train.get_checkpoint_state('./')
329 | saver.restore(sess1, ckpt.model_checkpoint_path)
330 | ```
331 |
332 | ### 2.3 注意事项
333 |
334 | **注意事项1**:
335 |
336 | 当一个图对应多个会话时,在不同的会话中使用图的Saver对象保存变量时,并不会互相影响。
337 |
338 | 例如,对于两个会话`sess1`、`sess2`分别操作一个图`g`中的变量,并存储在不同文件中:
339 |
340 | ```python
341 | with tf.Graph().as_default() as g:
342 | var = tf.Variable(1, name='var')
343 | saver = tf.train.Saver()
344 |
345 |
346 | with tf.Session(graph=g) as sess1:
347 | sess1.run(tf.global_variables_initializer())
348 | sess1.run(var.assign_add(1))
349 |
350 | saver.save(sess1, './model1/model.ckpt')
351 |
352 |
353 | with tf.Session(graph=g) as sess2:
354 | sess2.run(tf.global_variables_initializer())
355 | sess2.run(var.assign_sub(1))
356 |
357 | saver.save(sess2, './model2/model.ckpt')
358 | ```
359 |
360 | 当我们分别加载变量时,可以发现,并没有互相影响。如下:
361 |
362 | ```python
363 | with tf.Session(graph=g) as sess3:
364 | sess3.run(tf.global_variables_initializer())
365 | saver.restore(sess3, './model1/model.ckpt')
366 | print(sess3.run(var)) # >> 2
367 |
368 | with tf.Session(graph=g) as sess4:
369 | sess4.run(tf.global_variables_initializer())
370 | saver.restore(sess4, './model1/model.ckpt')
371 | print(sess4.run(var)) # >> 0
372 | ```
373 |
374 | **注意事项2**:
375 |
376 | 当我们在会话中恢复变量时,必须要求会话所绑定的图与所要恢复的变量所代表的图一致,这里我们需要知道什么样的图时一致。
377 |
378 | 例如:
379 |
380 | ```python
381 | with tf.Graph().as_default() as g1:
382 | a = tf.Variable([1, 2, 3])
383 |
384 | with tf.Graph().as_default() as g2:
385 | b = tf.Variable([1, 2, 3])
386 | ```
387 |
388 | 上面这两个图是一致的,虽然其绑定的Python变量不同。
389 |
390 | ```python
391 | with tf.Graph().as_default() as g1:
392 | a = tf.Variable([1, 2, 3])
393 |
394 | with tf.Graph().as_default() as g2:
395 | b = tf.Variable([1, 2, 3], name='var')
396 | ```
397 |
398 | 上面这两个图是不一样的,因为使用了不同的`name`。
399 |
400 | ```python
401 | with tf.Graph().as_default() as g1:
402 | a = tf.Variable([1, 2, 3], name='a')
403 | b = tf.Variable([4, 5], name='b')
404 |
405 | with tf.Graph().as_default() as g2:
406 | c = tf.Variable([4, 5], name='b')
407 | d = tf.Variable([1, 2, 3], name='a')
408 | ```
409 |
410 | 这两个图是一致的。
411 |
412 | ```python
413 | with tf.Graph().as_default() as g1:
414 | a = tf.Variable([1, 2, 3])
415 | b = tf.Variable([4, 5])
416 |
417 | with tf.Graph().as_default() as g2:
418 | c = tf.Variable([4, 5])
419 | d = tf.Variable([1, 2, 3])
420 | ```
421 |
422 | 这两个图是不一致。看起来,两个图一模一样,然而Tensorflow会给每个没有`name`的Op进行命名,两个图由于均使用了2个Variable,并且都没有主动命名,所以在`g1`中a的`name`与`g2`中c的`name`不同,`g1`中b的`name`与`g2`中d的`name`不同。
423 |
424 | **注意事项3**:
425 |
426 | 当两个图只有部分结构一样时,可以恢复部分变量,如下:
427 |
428 | ~~~python
429 | # g1 图如下,此时我们如果保存了 g1 中的变量那么在其他结构相似的图中也可以恢复
430 | with tf.Graph().as_default() as g1:
431 | a = tf.Variable(10.)
432 | b = tf.Variable(20.)
433 | c = a + b
434 | saver1 = tf.train.Saver()
435 |
436 | # g2 图中变量a、b与 g1 中完成一样,此时我们在`tf.train.Saver`中传入需要
437 | # 管理的变量a、b,那么我们就可以在会话中恢复a、b
438 | with tf.Graph().as_default() as g2:
439 | a = tf.Variable(10.)
440 | b = tf.Variable(10.)
441 | d = tf.Variable(5.)
442 | c = a + b
443 | saver2 = tf.train.Saver(var_list=[a, b])
444 | ~~~
445 |
446 | ## 3. 变量的值冻结到图中
447 |
448 | 当我们训练好了一个模型时,就意味着模型中的变量的值确定了,不在需要改变了。上述方法中我们需要分别将图与变量保存(或者保存变量时也保存了meta graph),这里我们也可以使用更简单的方法完成。即可以将所有变量固化成常量,随图一起保存。这样使用起来也更加简便,我们只需要导入图即可完成模型的完整导入。利用固化后的图参与构建新的图也变得容易了(不需要单独开启会话并加载变量了)。
449 |
450 | 实现上述功能,需要操作GraphDef中的节点,TensorFlow为我们提供了相关的API:`convert_variables_to_constants`。
451 |
452 | `convert_variables_to_constants`在`tensorflow.python.framework.graph_util`中,用法如下:
453 |
454 | ```python
455 | # 返回一个新的图
456 | convert_variables_to_constants(
457 | sess, # 会话
458 | input_graph_def, # 图定义
459 | output_node_names, # 输出节点(不需要的节点在生成的新图将被排除)(注意是节点而不是Tensor)
460 | variable_names_whitelist=None, # 指定的variable转成constant
461 | variable_names_blacklist=None) # 指定的variable不转成constant
462 | ```
463 |
464 | 例如:
465 |
466 | 将变量转化为常量,并存成图。
467 |
468 | ```python
469 | import tensorflow as tf
470 | from tensorflow.python.framework import graph_util
471 |
472 | with tf.Graph().as_default() as g:
473 | my_input = tf.placeholder(dtype=tf.int32, shape=[], name='input')
474 | var = tf.get_variable(name='var', shape=[], dtype=tf.int32)
475 | output = tf.add(my_input, var, name='output')
476 |
477 | with tf.Session(graph=g) as sess:
478 | sess.run(tf.global_variables_initializer())
479 | sess.run(var.assign(10))
480 |
481 | # 将变量转化为常量,返回一个新的图
482 | new_graph = graph_util.convert_variables_to_constants(
483 | sess,
484 | sess.graph_def,
485 | output_node_names=['output'])
486 | # 将新的图保存
487 | tf.train.write_graph(new_graph, '', 'graph.pb', as_text=False)
488 | ```
489 |
490 | 导入刚刚序列化的图:
491 |
492 | ```python
493 | with tf.Graph().as_default() as new_graph:
494 | x = tf.placeholder(dtype=tf.int32, shape=[], name='x')
495 | with tf.gfile.FastGFile('graph.pb', 'rb') as f:
496 | graph_def = tf.GraphDef()
497 | graph_def.ParseFromString(f.read())
498 | g_out = tf.import_graph_def(
499 | graph_def,
500 | input_map={'input:0': x},
501 | return_elements=['output:0'])
502 |
503 | with tf.Session(graph=new_graph) as sess:
504 | print(sess.run(g_out[0], feed_dict={x: 5})) # >> 15
505 | ```
506 |
507 | ## 4. SavedModel
508 |
509 | 上述模型存取的方法是有局限性的,仅仅适合于Python环境下的TensorFlow模型存取,而且模型的图与变量的保存是分开进行的,这在训练模型的时候是比较方便的,但适用范围较窄且麻烦。而SavedModel的适用范围更广,它是一种与语言无关,可恢复的密封式序列化格式。它可以同时保存图与变量,操作也更加方便。SavedModel 可让较高级别的系统和工具创建、使用和变换 TensorFlow 模型。TensorFlow 提供了多种与 SavedModel 交互的机制,如 tf.saved_model API、Estimator API 和 CLI,但通常只在模型训练完成之后才使用SaveModel保存模型。
510 |
511 | ### 4.1 模型保存
512 |
513 | SavedModel的用法比较简单,其功能模块在TensorFlow的`saved_model`下,主要功能包含在`builder`和、`loader`两个子模块中,分别用于保存和加载SavedModel。保存模型的用法如下:
514 |
515 | ~~~python
516 | ...
517 | builder = tf.saved_model.builder.SavedModelBuilder('/tmp/save_path')
518 | with tf.Session() as sess:
519 | ...
520 | builder.add_meta_graph_and_variables(sess, ['train'])
521 | builder.save()
522 | ~~~
523 |
524 | 可以看到用法很简单,首先构建`SavedModelBuilder`实例对象,构建时需要传入保存路径(注意这个路径文件夹中必须是没有文件的),然后调用`add_meta_graph_and_variables`方法将元图与变量加入`SavedModelBuilder`实例对象中,此时需要传入`Session`以及`tags`标记,最后调用`save`方法完成序列化。这里需要注意的是`tags`是必须的,用来标记存储的元图和变量,因为我们也可以使用一个saved_model多次存储模型,这时候需使用`tag`加以区分。例如,我们需要存两个元图,如下:
525 |
526 | ~~~python
527 | ...
528 | builder = tf.saved_model.builder.SavedModelBuilder('/tmp/save_path')
529 | with tf.Session() as sess:
530 | ...
531 | builder.add_meta_graph_and_variables(sess, ['train'])
532 |
533 | with tf.Session() as sess:
534 | ...
535 | builder.add_meta_graph(['serve'])
536 |
537 | builder.save()
538 | ~~~
539 |
540 | 这里需要注意,`add_meta_graph_and_variables`方法只能调用一次,即变量只能存储一次,但元图可以存储很多次。
541 |
542 | TensorFlow的saved_model模块下的`tag_constants`内置了常用`tag`常量,通常的我们使用这些常量即可,一般不需要自己自定义,这样可以使我们的程序设计更加一致。如下:
543 |
544 | ~~~python
545 | tf.saved_model.tag_constants.SERVING # 对应字符串 `serve`
546 | tf.saved_model.tag_constants.GPU # 对应字符串 `gpu`
547 | tf.saved_model.tag_constants.TPU # 对应字符串`tpu`
548 | tf.saved_model.tag_constants.TRAINING # 对应字符串 `train`
549 | ~~~
550 |
551 | **注意**:当保存模型时使用了多个`tag`,那么在恢复模型时也需要多个一样的`tag`。
552 |
553 | ### 4.2 模型恢复
554 |
555 | 模型的恢复不依赖于编程语言,在Python中的用法如下:
556 |
557 | ~~~python
558 | ...
559 | with tf.Session() as sess:
560 | tf.saved_model.loader.load(sess, ['serve'], '/tmp/save_path')
561 | ...
562 | ~~~
563 |
564 | 即调用`loader`模块下的`load`方法,传入`Session`对象,需要恢复的模型的`tags`,以及模型的保存路径即可。恢复完成之后,可以通过图的一些方法获取到`op`与`tensor`。
565 |
566 | 通常的,我们一般使用Python语言构建与训练模型,完成之后需要将模型部署到服务器,这时候我们可以使用TensorFlow Serving通过CLI来恢复元图与变量,如下:
567 |
568 | ~~~shell
569 | tensorflow_model_server
570 | --port=port-numbers
571 | --model_name=your-model-name
572 | --model_base_path=your_model_base_path
573 | ~~~
574 |
575 | ### 4.3 Signature
576 |
577 | 上述使用SavedModel存取模型时存在一个问题就是模型的入口与出口部分我们没有显式的标记出来,虽然我们可以在加载完成模型之后使用诸如`get_operation_by_name`的方法获取到模型中节点或张量,但这往往不太方便,也使得模型的设计与部署的耦合性较高。如何使得模型的训练时与应用时在模型的使用上解耦呢?SavedModel提供了Signature来解决这一问题。
578 |
579 | 具体来讲就是在保存模型时,对模型的输入、输出张量签名。类似于给张量一个别名,用法如下:
580 |
581 | ~~~python
582 | # 构建一个图,图中的op使用的name如x、y、z不宜在部署时使用
583 | with tf.Graph().as_default() as g:
584 | x = tf.placeholder(dtype=tf.int32, shape=[], name='x')
585 | y = tf.placeholder(dtype=tf.int32, shape=[], name='y')
586 | z = tf.add(x, y, name='z')
587 |
588 | builder = tf.saved_model.builder.SavedModelBuilder('/tmp/save_path')
589 | # 给图中输入输出张量构建一个签名
590 | inputs = {'inputs_x': tf.saved_model.utils.build_tensor_info(x),
591 | 'inputs_y': tf.saved_model.utils.build_tensor_info(y)}
592 | outputs = {'outputs': tf.saved_model.utils.build_tensor_info(z)}
593 | signature = tf.saved_model.signature_def_utils.build_signature_def(inputs, outputs)
594 | with tf.Session(graph=g) as sess:
595 | sess.run(tf.global_variables_initializer())
596 | builder.add_meta_graph_and_variables(
597 | sess,
598 | [tf.saved_model.tag_constants.SERVING],
599 | {'my_signature': signature}) # 保存模型时,加上签名的部分
600 | builder.save()
601 | ~~~
602 |
603 | 上面的代码是模型保存的部分,模型加载的时候就可以使用签名方便的获取输入、输出张量了。如下:
604 |
605 | ~~~python
606 | with tf.Graph().as_default() as new_graph:
607 | with tf.Session(graph=new_graph) as sess:
608 | meta_graph_def = tf.saved_model.loader.load(
609 | sess,
610 | [tf.saved_model.tag_constants.SERVING],
611 | '/tmp/save_path')
612 | # 获取到签名
613 | signature = meta_graph_def.signature_def['my_signature']
614 | # 根据签名可获取TensorInfo,然后利用相关方法可以得到对应的Tensor
615 | inputs_x = tf.saved_model.utils.get_tensor_from_tensor_info(
616 | signature.inputs['inputs_x'])
617 | inputs_y = tf.saved_model.utils.get_tensor_from_tensor_info(
618 | signature.inputs['inputs_y'])
619 | outputs = tf.saved_model.utils.get_tensor_from_tensor_info(
620 | signature.outputs['outputs'])
621 |
622 | print(sess.run(outputs, feed_dict={inputs_x: 5, inputs_y: 10})) # >>> 15
623 | ~~~
624 |
625 | 上面的代码看起来有些复杂,这里简单的解释一下,首先使用`tf.saved_model.loader.load`载入元图与变量,这时候返回元图的定义。元图定义中包含了签名信息,所以访问元图的`signature_def`属性可以得到所有签名,然后取出对应的我们定义的签名即可。签名信息中包含了`inputs`与`outputs`等属性,存储了`TensorInfo`对象,这时候使用`tf.saved_model.utils.get_tensor_from_tensor_info`方法可以根据`TensorInfo`取出对应的张量。
626 |
627 | **注意**:签名可以设置多个,代表一个图中不同的输入输出。这也是为什么需要给签名一个名字的原因。
628 |
629 | 就像`tags`一样,SavedModel签名也有一些内置的值,推荐使用这些值,如下:
630 |
631 | ~~~python
632 | tf.saved_model.signature_constants.CLASSIFY_INPUTS # `inputs`
633 | tf.saved_model.signature_constants.CLASSIFY_METHOD_NAME # `tensorflow/serving/classify`
634 | ...
635 | ~~~
636 |
637 | #### 简化版的带Signature的模型保存
638 |
639 | 上述代码写起来比较复杂,事实上,在模型保存部分,还有更简单的写法,但这种写法只支持保存一个签名。用法如下:
640 |
641 | ~~~python
642 | # 假设x、y与z分别为输入输出张量
643 | ...
644 | with tf.Session() as sess:
645 | ...
646 | tf.saved_model.simple_save(
647 | sess,
648 | '/tmp/save_path',
649 | inputs={'inputs_x': x, 'inputs_y': y}, outputs={'output': z})
650 | ~~~
651 |
652 | 可以看到上面的写法屏蔽了很多细节,要更加清晰、明了,但要注意的是,其在保存模型的时候也是使用了`tags`的,其`tag`为`tf.saved_model.tag_constants.SERVING`。签名的名字默认使用的是`tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY`。这些信息在模型读取时依然会用到。
653 |
654 | ## 5. 使用 CLI 检查并执行 SavedModel
655 |
656 | 一般的成功安装TensorFlow之后,也同样安装好了`saved_model_cli`,这是一个终端中检查并执行SavedModel的工具。主要功能包括查看SavedModel签名以及执行SavedModel中的图。目前支持以下三个命令:
657 |
658 | * `show`:显示所有关于指定的SavedModel的全部信息。
659 | * `run`:输入数据到元图并执行得到结果
660 | * `scan`:浏览
661 |
662 | ### 5.1 show 命令
663 |
664 | 使用show命令可以显示指定的SavedModel所有的`tag-set`,用法如下:
665 |
666 | ~~~shell
667 | $ saved_model_cli show --dir savedmodel_path
668 | ~~~
669 |
670 | `savedmodel_path`是指SavedModel所在的文件夹路径。执行之后显示如下结果:
671 |
672 | ~~~txt
673 | The given SavedModel contains the following tag-sets:
674 | serve
675 | serve, gpu
676 | ~~~
677 |
678 | 代表模型中有两个`tag-set`,一个是`serve`,另一个是`serve, gpu`。
679 |
680 | 我们可以根据`tag-set`获取到指定的`tag-set`下的`MetaGraphDef`,这是非常方便的,因为提供这样的工具可以在我们不知道模型详细结构的情况下方便的调用模型。
681 |
682 | 使用第二个`tag-set`获取`MetaGraphDef`的命令如下:
683 |
684 | ~~~shell
685 | $ saved_model_cli show --dir savedmodel_path --tag_set serve,gpu
686 | ~~~
687 |
688 | 执行后显示结果如下:
689 |
690 | ~~~tex
691 | The given SavedModel `MetaGraphDef` contains `SignatureDefs` with the
692 | following keys:
693 | SignatureDef key: "classify_x2_to_y3"
694 | SignatureDef key: "classify_x_to_y"
695 | SignatureDef key: "regress_x2_to_y3"
696 | SignatureDef key: "regress_x_to_y"
697 | SignatureDef key: "regress_x_to_y2"
698 | SignatureDef key: "serving_default"
699 | ~~~
700 |
701 | 这代表保存的某一个元图中包含了6个`SignatureDef`,也就是说我们可以在这个图中使用6个输入输出。
702 |
703 | 如果想查看某一个`SignatureDef`中输入、输出的签名,则可以执行如下命令:
704 |
705 | ~~~shell
706 | $ saved_model_cli show --dir savedmodel_path --tag_set serve,gpu --signature_def serving_default
707 | ~~~
708 |
709 | 执行后的结果如下:
710 |
711 | ~~~tex
712 | MetaGraphDef with tag-set: 'serve, gpu' contains the following SignatureDefs:
713 |
714 | signature_def['serving_default']:
715 | The given SavedModel SignatureDef contains the following input(s):
716 | inputs['inputs_x'] tensor_info:
717 | dtype: DT_INT32
718 | shape: ()
719 | name: x:0
720 | inputs['inputs_y'] tensor_info:
721 | dtype: DT_INT32
722 | shape: ()
723 | name: y:0
724 | The given SavedModel SignatureDef contains the following output(s):
725 | outputs['outputs'] tensor_info:
726 | dtype: DT_INT32
727 | shape: ()
728 | name: z:0
729 | Method name is: tensorflow/serving/predict
730 | ~~~
731 |
732 | 这个结果详细展示了输入、输出签名等信息。可以方便我们调用。
733 |
734 | **提示**:可以使用`saved_model_cli show --dir /tmp/saved_model_dir --all`命令查看所有信息。
735 |
736 | ### 5.2 run命令
737 |
738 | 调用`run`命令可以运行计算图,并可以将结果显示或保存。同时,为了使模型运行,我们也需要传递输入数据进入模型,`run`命令提供了以下三种方式将输入传递给模型:
739 |
740 | * `--inputs`:接受numpy ndarray文件数据作为输入。
741 | * `--input_exprs`:接受Python表达式作为输入,即将命令行的输入解析为了Python表达式。
742 | * `--input_examples`:接受`tf.train.Example`作为输入。
743 |
744 | **--inputs**
745 |
746 | INPUT 采用以下格式之一:
747 |
748 | - `=`
749 | - `=[]`
750 |
751 | 您可能会传递多个 INPUT。如果您确实要传递多个输入,请使用分号分隔每个 INPUT。
752 |
753 | `saved_model_cli` 使用 `numpy.load` 加载文件名。文件名可以是以下任何一种格式:
754 |
755 | - `.npy`
756 | - `.npz`
757 | - pickle 格式
758 |
759 | **--inputs_exprs**
760 |
761 | 要通过 Python 表达式传递输入,请指定 `--input_exprs` 选项。这对于您目前没有数据文件的情形而言非常有用,但最好还是用一些与模型的 `SignatureDef` 的 dtype 和形状匹配的简单输入来检查模型。例如:
762 |
763 | ```shell
764 | `=[[1],[2],[3]]`
765 | ```
766 |
767 | 除了 Python 表达式之外,您还可以传递 numpy 函数。例如:
768 |
769 | ```shell
770 | `=np.ones((32,32,3))`
771 | ```
772 |
773 | (请注意,`numpy` 模块已可作为 `np` 提供。)
774 |
775 | **--inputs_examples**
776 |
777 | 要将 `tf.train.Example` 作为输入传递,请指定 `--input_examples` 选项。对于每个输入键,它都基于一个字典列表,其中每个字典都是 `tf.train.Example` 的一个实例。不同的字典键代表不同的特征,而相应的值则是每个特征的值列表。例如:
778 |
779 | ```shell
780 | `=[{"age":[22,24],"education":["BS","MS"]}]`
781 | ```
782 |
783 | 例如:
784 |
785 | 您的模型只需添加 `x1` 和 `x2` 即可获得输出 `y`。模型中的所有张量都具有形状 `(-1, 1)`。您有两个 `npy` 文件:`/tmp/my_data1.npy`,其中包含多维数组 `[[1], [2], [3]]`。`/tmp/my_data2.npy`,其中包含另一个多维数组 `[[0.5], [0.5], [0.5]]`。:
786 |
787 | ~~~shell
788 | $ saved_model_cli run --dir /tmp/saved_model_dir --tag_set serve --signature_def x1_x2_to_y --inputs x1=/tmp/my_data1.npy;x2=/tmp/my_data2.npy --outdir /tmp/out
789 | Result for output key y:
790 | [[ 1.5]
791 | [ 2.5]
792 | [ 3.5]]
793 | ~~~
794 |
795 | 以下命令用 Python 表达式替换输入 `x2`:
796 |
797 | ~~~shell
798 | $ saved_model_cli run --dir /tmp/saved_model_dir --tag_set serve --signature_def x1_x2_to_y --inputs x1=/tmp/my_data1.npz[x] --input_exprs 'x2=np.ones((3,1))'
799 | Result for output key y:
800 | [[ 2]
801 | [ 3]
802 | [ 4]]
803 | ~~~
804 |
805 | **保存输出**
806 |
807 | 默认情况下,SavedModel CLI 将输出写入 stdout。如果目录传递给 `--outdir` 选项,则输出将被保存为在指定目录下以输出张量键命名的 npy 文件。
808 |
809 | 使用 `--overwrite` 覆盖现有的输出文件。
810 |
811 | #### SavedModel 目录结构
812 |
813 | 当您以 SavedModel 格式保存模型时,TensorFlow 会创建一个由以下子目录和文件组成的 SavedModel 目录:
814 |
815 | ```
816 | assets/
817 | assets.extra/
818 | variables/
819 | variables.data-?????-of-?????
820 | variables.index
821 | saved_model.pb|saved_model.pbtxt
822 | ```
823 |
824 | 其中:
825 |
826 | - `assets` 是包含辅助(外部)文件(如词汇表)的子文件夹。资源被复制到 SavedModel 的位置,并且可以在加载特定的 `MetaGraphDef` 时读取。
827 | - `assets.extra` 是一个子文件夹,其中较高级别的库和用户可以添加自己的资源,该资源与模型共存,但不会被图加载。此子文件夹不由 SavedModel 库管理。
828 | - `variables` 是包含 `tf.train.Saver` 的输出的子文件夹。
829 | - `saved_model.pb` 或 `saved_model.pbtxt` 是 SavedModel 协议缓冲区。它包含作为 `MetaGraphDef` 协议缓冲区的图定义。
830 |
831 | 单个 SavedModel 可以表示多个图。在这种情况下,SavedModel 中所有图共享一组检查点(变量)和资源。例如,下图显示了一个包含三个 `MetaGraphDef` 的 SavedModel,它们三个都共享同一组检查点和资源:
832 |
833 | 
834 |
835 | 每个图都与一组特定的标签相关联,可在加载或恢复操作期间方便您进行识别。r
--------------------------------------------------------------------------------
/ch10/ch10-eager.md:
--------------------------------------------------------------------------------
1 | TensorFlow下的eager模式是一个命令式编程模式。无需构建图,所有的操作会立即返回结果,所以也不需要会话。eager模型的优点包括:直观的界面、更容易调试、简单的流程控制。eager模式支持大部分的TensorFlow操作与GPU加速。缺点是执行效率尚不够高。在TensorFlow 1.4版本中,第一次引入了Eager Execution,在TensorFlow 1.5版本中将其加入`tf.contrib`模块下,在TensorFlow 1.7版本中加入核心API。
2 |
3 | ## 开启eager模式
4 |
5 | 在程序最开始添加`tf.enable_eager_execution()`语句即可开启eager模式。需要注意的是,`tf.enable_eager_execution()`语句之前不能有任何TensorFlow操作与张量的构建语句。也不要将此语句添加到程序调用的模块中。
6 |
7 | 用法如下:
8 |
9 | ~~~python
10 | import tensorflow as tf
11 |
12 | tf.enable_eager_execution()
13 | ~~~
14 |
15 | 可以使用`tf.executing_eagerly()`语句查看当前是否开启了eager模式,开启则返回`True`,否则返回`False`。
16 |
17 | 现在就可以运行TensorFlow操作,之后将立即返回结果:
18 |
19 | ~~~python
20 | const = tf.constant(5)
21 | print(const) # >>> tf.Tensor(5, shape=(), dtype=int32)
22 |
23 | res = tf.multiply(const, const)
24 | print('result is %d' % res) # >>> result is 25
25 | ~~~
26 |
27 | 由于所有的执行结果会立即呈现在Python环境中,所以调试起来会很方便。
28 |
29 | 注意:eager模式一旦开启就无法关闭。
30 |
31 | ## 基本用法
32 |
33 | 开启eager模式之后,TensorFlow中的大部分操作都可以使用,但操作也变得有一些不同,主要是Tensor对象的操作与流程控制部分有较大变化。
34 |
35 | ### Tensor
36 |
37 | 对于Tensor对象,现在它可以与Python、Numpy很好的进行协作(并不能与其它科学计算库很好的协作)。例如我们不仅可以将Numpy数组作为输入传入TensorFlow的操作中,现在也可以将Tensor传入Numpy的操作中,事实上传入Numpy操作是调用了`tf.Tensor.numpy()`方法将值转换为了`ndarray`对象。例如:
38 |
39 | ~~~python
40 | a = tf.constant(5)
41 | b = np.array(10)
42 |
43 | tf_res = tf.multiply(a, b) # >>> tf.Tensor(50, shape=(), dtype=int32)
44 | np_res = np.multiply(a, b) # >>> 50
45 | ~~~
46 |
47 | 显式的调用`tf.Tensor.numpy()`可以得到Tensor对应的ndarray对象,用于更多其他操作例如:
48 |
49 | ~~~python
50 | import tensorflow as tf
51 | from PIL import Image
52 |
53 | tf.enable_eager_execution()
54 |
55 | img = tf.random_uniform([28, 28, 3], maxval=255, dtype=tf.int32)
56 | img = tf.cast(img, tf.uint8)
57 |
58 | Image.fromarray(img.numpy()) # 调用tf.Tensor.numpy()获取ndarray对象并转化为PIL的Image对象
59 | ~~~
60 |
61 | **注意**:在Eager模式下,所有操作的`name`属性都是没有意义的。也就是说虽然你仍然可以给操作设置`name`,但实际上毫无意义。
62 |
63 | ### 变量
64 |
65 | Eager下创建变量的用法也有一些不同,最简单的创建变量的方法是使用`tf.get_variable`创建变量,这时候需要注意变量是拥有`name`属性的,但意义不大。用法如下:
66 |
67 | ~~~python
68 | var = tf.get_variable('var', shape=[])
69 |
70 | print(var) # >>>
71 | ~~~
72 |
73 | Eager模式下不需要初始化变量了。使用`tf.get_variable`创建变量的用法与传统用法并不完全一致,在eager模式下`tf.get_variable`只能用来创建变量,不能获取已经创建好的变量。
74 |
75 | 除此之外还可以使用如下方法创建变量:
76 |
77 | ~~~python
78 | import tensorflow.contrib.eager as tfe
79 |
80 | var = tfe.Variable(10)
81 | print(var) # >>>
82 | ~~~
83 |
84 | ### 运算
85 |
86 | 在eager模式下,基本的算术运算、逻辑运算、位运算规则与Python下基本是相同的,但在执行比较运算与赋值运算时需要注意,有些运算符没有重载,有些运算结果与我们想象的不太一样:
87 |
88 | 在比较运算中,需要注意:
89 |
90 | ```python
91 | tf.constant(0) == 0 # False
92 |
93 | tf.constant(0) <= 0 # True
94 | tf.constant(0) >= 0 # True
95 | ```
96 |
97 | 为了不出现这种问题,通常的我们首先将比较运算的输入数据的类型转化一致之后再进行比较如下:
98 |
99 | ~~~python
100 | # 方法一:将Tensor转化为Python数据类型
101 | int(tf.constant(0)) == 0 # True
102 |
103 | # 方法二:利用比较函数进行比较
104 | tf.equal(tf.constant(0), 0)
105 |
106 | # 方法三:利用ndarray进行比较
107 | tf.constant(0).numpy() == 0
108 | ~~~
109 |
110 | TensorFlow变量在赋值运算中需要注意:
111 |
112 | * TensorFlow变量不能直接使用`=`赋值
113 | * TensorFlow变量不支持使用`+=`等赋值运算符重载
114 |
115 | 如下:
116 |
117 | ~~~python
118 | var = tfe.Variable(10)
119 |
120 | var.assign(5) # 正确用法:赋值
121 | var.assign_add(5) # 正确用法:增量赋值
122 |
123 | var += 5 # 错误用法,不支持此运算符重载
124 | var = 5 # 错误用法,此时相关于给Python变量var赋值为5,并没有使用TensorFlow中的变量
125 | ~~~
126 |
127 | 但在Tensor中是支持增量赋值运算符的(不支持直接赋值运算符),如下:
128 |
129 | ~~~python
130 | var = tf.constant(10)
131 | var += 5
132 | print(var) # >>> tf.Tensor(15, shape=(), dtype=int32)
133 |
134 | var -= 5
135 | print(var) # >>> tf.Tensor(10, shape=(), dtype=int32)
136 | ~~~
137 |
138 | ### 流程控制
139 |
140 | 在符号式编程模式下,流程控制是较为复杂的操作,往往需要使用不太直观的`tf.cond`、`tf.case`、`tf.while_loop`流程控制函数进行操作。而在eager模式下,直接使用Python的流程控制语句即可,如下使用TensorFlow实现一个fizzbuzz游戏(规则:输入一系列整数,当数字为3的倍数的时候,使用“Fizz”替代数字,当为5的倍数用“Buzz”代替,既是3的倍数又是5的倍数时用“FizzBuzz”替代。):
141 |
142 | ~~~python
143 | def fizzbuzz(max_num):
144 | max_num = tf.convert_to_tensor(max_num)
145 | for i in tf.range(max_num):
146 | if int(i % 3) == 0 and int(i % 5) == 0:
147 | print('fizzbuzz')
148 | elif int(i % 3) == 0:
149 | print('fizz')
150 | elif int(i % 5) == 0:
151 | print('buzz')
152 | else:
153 | print(int(i))
154 | ~~~
155 |
156 | ### 变量存取
157 |
158 | 使用 Graph Execution 时,程序状态(如变量)存储在全局集合中,它们的生命周期由 [`tf.Session`](https://tensorflow.google.cn/api_docs/python/tf/Session) 对象管理。相反,在 Eager Execution 期间,状态对象的生命周期由其对应的 Python 对象的生命周期决定。
159 |
160 | 在Eager模式下,变量的保存使用`tf.train.Checkpoint`的对象来完成,而无法使用`tf.train.Saver`,同时`tf.train.Checkpoint`即支持Graph Execution,也支持Eager Execution,是TensorFlow最新推荐的模型变量保存方法。
161 |
162 | 二者区别:
163 |
164 | * `tf.train.Saver`存取变量时,实际上是依据`variable.name` 进行存取与匹配的方法,只能在图模式下使用;
165 | * `tf.train.Checkpoint`则是存取Python对象与边、节点之间的依赖关系,可以在两种模式下使用。
166 |
167 | `tf.train.Checkpoint`用法简单,首先使用`tf.train.Checkpoint`实例化一个对象,同时传入需要保存的变量,在完成一系列操作之后,可以调用`tf.train.Checkpoint.save`方法保存模型。恢复模型只需要调用`tf.train.Checkpoint.restore`即可,如下:
168 |
169 | ~~~python
170 | x = tf.get_variable('x', shape=[])
171 |
172 | checkpoint = tf.train.Checkpoint(x=x) # 声明将变量x保存为"x",可以保存为任意合法的字符串
173 |
174 | x.assign(2.) # 给变量x重新复制
175 | save_path = checkpoint.save('./ckpt/') # 保存变量到指定目录,此时x的值2就保存下来了
176 |
177 | x.assign(11.) # 再次改变x的值
178 |
179 | checkpoint.restore(save_path) # 恢复变量
180 |
181 | print(x) # >>> 2.0
182 | ~~~
183 |
184 | ### 自动求微分
185 |
186 | 自动微分对于机器学习算法来讲是很有用的,使用 Graph Execution 时,我们可以借助图本身的性质对变量集合中的所有变量进行自动微分,而在 Eager Execution 期间,则需要使用另一个工具——梯度磁带。当我们定义了一个梯度磁带时,正向传播操作都会记录到磁带中(所以会降低性能,如无需要请勿开启磁带)。要计算梯度时,反向播放磁带即可,默认的磁带只能反向播放一次(求一次梯度)。用法如下:
187 |
188 | ~~~python
189 | w = tf.get_variable('w', shape=[])
190 | w.assign(5.)
191 |
192 | with tf.GradientTape() as gt:
193 | loss = w * w
194 | print(gt.gradient(loss, w)) # >>> tf.Tensor(10.0, shape=(), dtype=float32)
195 | ~~~
196 |
197 | 默认的磁带只能反向使用一次,如果需要多次使用则需要设置`persistent=True`,如下:
198 |
199 | ~~~python
200 | w = tf.get_variable('w', shape=[])
201 | w.assign(5.)
202 |
203 | with tf.GradientTape(persistent=True) as gt:
204 | loss = w * w
205 |
206 | print(gt.gradient(loss, w))
207 | print(gt.gradient(loss, w))
208 | ~~~
209 |
210 | 磁带也可以嵌套,求二阶或高阶梯度,如下:
211 |
212 | ~~~python
213 | x = tf.constant(3.0)
214 | with tf.GradientTape() as g:
215 | with tf.GradientTape() as gg:
216 | gg.watch(x) # watch张量之后,可对张量求梯度
217 | y = x * x
218 |
219 | dy_dx = gg.gradient(y, x)
220 | print(dy_dx) # >>> 6
221 | d2y_dx2 = g.gradient(dy_dx, x)
222 | print(d2y_dx2) # >>> 2
223 | ~~~
224 |
225 | 注意事项:
226 |
227 | * 只有磁带上下文管理器中操作会被记录,以外的部分不会被记录,所以也无法对以外的操作求得梯度,结果会是`None`。
228 | * 默认的磁带只能求一次梯度。
229 | * 在磁带的上下文管理器中连续调用求梯度的方法会存在性能问题,这会使得磁带记录上每次只需的操作,不断增加内存和CPU。
230 | * 默认的求梯度时只能对变量求梯度,也可以对张量求梯度,需要调用`tf.GradientTape.watch`方法加入求梯度的量中即可。
231 |
232 | ### TensorBoard
233 |
234 | 在eager模式下,TensorBoard创建事件文件并获取摘要的方法稍有不同。也就是说与图模式下的用法不兼容,但过程类似,创建摘要的一般步骤为:
235 |
236 | * 创建一个或多个事件文件,使用`tf.contrib.summary.create_file_writer`。
237 | * 将某一个事件文件置为默认,使用`writer.as_default`。
238 | * 在训练模型的代码处获取之后设置摘要获取频次,可使用`tf.contrib.summary.record_summaries_every_n_global_steps`、`tf.contrib.summary.always_record_summaries`分别设置摘要频次与每次摘要。
239 | * 给指定张量设置摘要。
240 |
241 | 具体如下:
242 |
243 | ~~~python
244 | # 创建global_step
245 | gs = tf.train.get_or_create_global_step()
246 | # 创建事件文件
247 | writer = tf.contrib.summary.create_file_writer('/tmp/log')
248 |
249 | # 将writer设置为默认writer(当存在多个writer时可用此方法切换writer)
250 | with writer.as_default():
251 | # 执行训练循环
252 | for i in range(train_steps):
253 | # 每100步执行一次提取摘要,默认的此方法会调用
254 | # `tf.train.get_or_create_global_step`查看当前global_step数
255 | with tf.contrib.summary.record_summaries_every_n_global_steps(100):
256 | # 在此处写训练代码
257 | ...
258 | ...
259 | ...
260 | # 添加摘要
261 | tf.contrib.summary.scalar('loss', loss)
262 | # global_step增加1
263 | gs.assign_add(1)
264 | ~~~
265 |
266 |
267 |
268 | ### 其它
269 |
270 | * 在eager模式下,`tf.placeholder`无法使用。
271 | * 在eager模式下, 由于所有语句均顺序执行,所以也不再需要控制依赖`tf.control_dependencies`进行强制执行顺序。
272 | * `name_scope`与`variable_scope`的也几乎没作用了,除了可以给变量的name添加前缀以外。
273 | * 在eager模式下,变量集合不被支持了,执行`tf.trainable_variables()`等语句会出错。
274 | * ...
275 |
276 |
--------------------------------------------------------------------------------
/ch11/ch11-tensorboard.md:
--------------------------------------------------------------------------------
1 | TensorBoard是TensorFlow自带的一个强大的可视化工具,也是一个web应用程序套件。在众多机器学习库中,TensorFlow是目前唯一自带可视化工具的库,这也是TensorFlow的一个优点。学会使用TensorBoard,将可以帮助我们构建复杂模型。
2 |
3 | 这里需要理解“可视化”的意义。“可视化”也叫做数据可视化。是关于数据之视觉表现形式的研究。这种数据的视觉表现形式被定义为一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。例如我们需要可视化算法运行的错误率,那么我们可以取算法每次训练的错误率,绘制成折线图或曲线图,来表达训练过程中错误率的变化。可视化的方法有很多种。但无论哪一种,均是对数据进行摘要(summary)与处理。
4 |
5 | 通常使用TensorBoard有三个步骤:
6 |
7 | - 首先需要在需要可视化的相关部位添加可视化代码,即创建摘要、添加摘要;
8 | - 其次运行代码,可以生成了一个或多个事件文件(event files);
9 | - 最后启动TensorBoard的Web服务器。
10 |
11 | 完成以上三个步骤,就可以在浏览器中可视化结果,Web服务器将会分析这个事件文件中的内容,并在浏览器中将结果绘制出来。
12 |
13 | 如果我们已经拥有了一个事件文件,也可以直接利用TensorBoard查看这个事件文件中的摘要。
14 |
15 | ## 1. 创建事件文件
16 |
17 | 在使用TensorBoard的第一个步骤中是添加可视化代码,即创建一个事件文件。创建事件文件需要使用`tf.summary.FileWriter()`。具体如下:
18 |
19 | ```python
20 | tf.summary.FileWriter(
21 | logdir, # 事件文件目录
22 | graph=None, # `Graph`对象
23 | max_queue=10, # 待处理事件和摘要的队列大小
24 | flush_secs=120, # 多少秒将待处理的事件写入磁盘
25 | graph_def=None, # [弃用参数]使用graph代替
26 | filename_suffix=None) # 事件文件的后缀名
27 | ```
28 |
29 | `tf.summary.FileWriter()`类提供了一个在指定目录创建了一个事件文件并向其中添加摘要和事件的机制。该类异步地更新文件内容,允许训练程序调用方法来直接从训练循环向文件添加数据,而不会减慢训练,即既可以**在训练的同时更新事件文件以供TensorBoard实时可视化**。
30 |
31 | 例如:
32 |
33 | ```python
34 | import tensorflow as tf
35 |
36 | writer = tf.summary.FileWriter('./graphs', filename_suffix='.file')
37 | writer.close()
38 | ```
39 |
40 | 运行上述代码将会在`'./graphs'`目录下生成一个后缀名是`'.file'`的文件,这个文件正是事件文件。
41 |
42 | **注意**:由于这个`writer`没有做任何操作,所以这是一个空的事件文件。如果不写`filename_suffix='.file'`这个属性,那么默认不创建空的事件文件。这时候如果使用TensorBoard的web服务器运行这个事件文件,看不到可视化了什么,因为这是一个空的事件文件。
43 |
44 | ## 2. TensorBoard Web服务器
45 |
46 | TensorBoard通过运行一个本地Web服务器,来监听6006端口。当浏览器发出请求时,分析训练时记录的数据,绘制训练过程中的图像。
47 |
48 | TensorBoard的Web服务器启动的命令是:`tensorboard`,主要的参数如下:
49 |
50 | ```shell
51 | --logdir # 指定一个或多个目录。tensorboard递归查找其中的事件文件。
52 | --host # 设置可以访问tensorboard服务的主机
53 | --port # 设置tensorboard服务的端口号
54 | --purge_orphaned_data # 是否清除由于TensorBoard重新启动而可能已被修改的数据。禁用purge_orphaned_data可用于调试修改数据。
55 | --nopurge_orphaned_data
56 | --reload_interval # 后端加载新产生的数据的间隔
57 | ```
58 |
59 | 假如现在当前目录下`graphs`文件夹中存在事件文件,可以使用如下命令打开tensorboard web服务器:
60 |
61 | ```shell
62 | tensorboard --logdir=./graphs
63 | ```
64 |
65 | 然后打开浏览器,进入`localhost:6006`即可查看到可视化的结果。
66 |
67 | ## 3. 可视化面板与操作详述
68 |
69 | 在Tensorflow中,summary模块用于可视化相关操作。TensorBoard目前内置8中可视化面板,即SCALARS、IMAGES、AUDIO、GRAPHS、DISTRIBUTIONS、HISTOGRAMS、EMBEDDINGS、TEXT。这8中可视化的主要功能如下。
70 |
71 | - SCALARS:展示算法训练过程中的参数、准确率、代价等的变化情况。
72 | - IMAGES:展示训练过程中记录的图像。
73 | - AUDIO:展示训练过程中记录的音频。
74 | - GRAPHS:展示模型的数据流图,以及训练在各个设备上消耗的内存与时间。
75 | - DISTRIBUTIONS:展示训练过程中记录的数据的分布图。
76 | - HISTOGRAMS:展示训练过程中记录的数据的柱状图。
77 | - EMBEDDINGS:展示词向量(如Word2Vec)的投影分布。
78 | - TEXT:展示文本内容
79 |
80 | 除此以外,TensorBoard还支持自定义可视化操作。可以参考[官方博客](https://developers.googleblog.cn/2017/09/tensorboard-api.html)。
81 |
82 | ### 3.1 GRAPHS
83 |
84 | TensorBoard可以查看我们构建的数据流图,这对于复杂的模型的构建有很大的帮助。
85 |
86 | #### 创建graph摘要
87 |
88 | 将数据流图可视化,需要将图传入summary的writer对象中。即在初始化`tf.summary.FileWriter()`时,给`graph`参数传入一个图。也可以在writer对象初始化之后使用`tf.add_graph()`给writer添加一个图。
89 |
90 | 如下,我们构建一个简单的图,并可视化:
91 |
92 | ```python
93 | import tensorflow as tf
94 |
95 | a = tf.constant(2)
96 | b = tf.constant(3)
97 | x = tf.add(a, b)
98 |
99 | with tf.Session() as sess:
100 | writer = tf.summary.FileWriter('./graphs', sess.graph)
101 | print(sess.run(x))
102 |
103 | writer.close()
104 | ```
105 |
106 | 在TensorBoard的GRAPHS中显示如下:
107 |
108 | 
109 |
110 | 图中`Const`、`Const_1`、`Add`表示的是节点的名称。
111 |
112 | 小圆圈表示为一个常量(constant)。椭圆形表示一个节点(OpNode)。箭头是一个引用边(reference edge)。当我们给代码中的op重命名之后,
113 |
114 | ```python
115 | import tensorflow as tf
116 |
117 | a = tf.constant(2, name='a')
118 | b = tf.constant(3, name='b')
119 | x = tf.add(a, b, name='add')
120 |
121 | with tf.Session() as sess:
122 | writer = tf.summary.FileWriter('./graphs', sess.graph)
123 | print(sess.run(x))
124 |
125 | writer.close() # 注意关闭fileWriter
126 | ```
127 |
128 | 在TensorBoard中显示如下:
129 |
130 | 
131 |
132 |
133 |
134 | 点击一个节点,可以查看节点的详情,如下查看add节点的详情:
135 |
136 | 
137 |
138 |
139 |
140 | 图中不仅仅有节点和箭头,TensorFlow有很多的用于图可视化的图形。所有的图像包括命名空间、op节点、没有连接关系的节点组、有连接关系的节点组、常量、摘要、数据流边、控制依赖边、引用边。具体如下:
141 |
142 | 
143 |
144 | #### Unconnected Series
145 |
146 | 一系列的不相连接的操作且`name`相似(除去系统自动给加的后缀以外的`name`部分相同)节点可以组成一个没有连接关系的节点组。
147 |
148 | ```python
149 | import tensorflow as tf
150 |
151 | with tf.Graph().as_default() as graph:
152 | a = tf.constant(2, name='a')
153 | b = tf.constant(3, name='b')
154 | x = tf.add(a, b, name='add')
155 | y = tf.add(a, b, name='add')
156 |
157 | with tf.Session() as sess:
158 | writer = tf.summary.FileWriter('./graphs')
159 | writer.add_graph(graph)
160 | writer.close()
161 | ```
162 |
163 | 可视化之后如下:
164 |
165 | 
166 |
167 | 这两个子图可以组成节点组,如下:
168 |
169 | 
170 |
171 | #### Connected Series
172 |
173 | 一系列的相连接的操作相同且name相同(不包括系统给加的后缀)节点可以组成一个有连接关系的节点组。
174 |
175 | ```python
176 | import tensorflow as tf
177 |
178 | with tf.Graph().as_default() as graph:
179 | a = tf.constant(2, name='a')
180 | b = tf.constant(3, name='b')
181 | x = tf.add(a, b, name='add')
182 | y = tf.add(a, x, name='add')
183 |
184 | with tf.Session() as sess:
185 | writer = tf.summary.FileWriter('./graphs')
186 | writer.add_graph(graph)
187 | writer.close()
188 | ```
189 |
190 | 可视化之后如下:
191 |
192 |
193 |
194 | 这两个子图可以组成节点组,如下:
195 |
196 |
197 |
198 | #### Dataflow edge
199 |
200 | ```python
201 | with tf.Graph().as_default() as graph:
202 | a = tf.constant(2, name='a')
203 | b = tf.constant(3, name='b')
204 | x = tf.add(a, b, name='add')
205 | y = tf.subtract(x, x, name='sub')
206 |
207 | with tf.Session() as sess:
208 | writer = tf.summary.FileWriter('./graphs')
209 | writer.add_graph(graph)
210 | writer.close()
211 | ```
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | #### Control Dependency edge
220 |
221 | ```python
222 | with tf.Graph().as_default() as graph:
223 | a = tf.constant(2, name='a')
224 | b = tf.constant(3, name='b')
225 | x = tf.add(a, b, name='add')
226 | with graph.control_dependencies([x]):
227 | y = tf.subtract(a, b, name='sub')
228 |
229 | with tf.Session() as sess:
230 | writer = tf.summary.FileWriter('./graphs')
231 | writer.add_graph(graph)
232 | writer.close()
233 | ```
234 |
235 |
236 |
237 | #### 其它
238 |
239 | 在图可视化面板中,还有有多常用的功能,比如:切换不同的图;查看某次迭代时运行的内存、时间;标注节点使用的计算设备;导入别的图等。
240 |
241 | ### 3.2 SCALARS
242 |
243 | SCALARS面板可以展示训练过程中某些标量数据的变化情况,例如模型参数的跟新情况、模型的正确率等。
244 |
245 | 使用scalars面板,主要是使用`tf.summary.scalar()`函数对张量进行采样,并返回一个包含摘要的protobuf的类型为string的0阶张量,函数的用法如下:
246 |
247 | ```python
248 | # name 节点名称,也将作为在SCALARS面板中显示的名称
249 | # tensor 包含单个值的实数数字Tensor
250 | tf.summary.scalar(name, tensor, collections=None)
251 | ```
252 |
253 | `tf.summary.scalar()`可以提取标量摘要,提取到的摘要需要添加到事件文件中,这时候可以使用`tf.summary.FileWriter.add_summary()`方法添加到事件文件中。`add_summary()`的用法如下:
254 |
255 | ```python
256 | add_summary(summary, global_step=None)
257 | ```
258 |
259 | 第一个参数是protobuf的类型为string的0阶张量摘要,第二个参数是记录摘要值的步数。例如训练100次模型,每次取一个摘要,这时候第二个参数就是第n次,这样可以得到标量随训练变化的情况。
260 |
261 | **注意**:`tf.summary.scalar()`获取到的并不是直接可以添加到事件文件中的数据,而是一个张量对象,必须在会话中执行了张量对象之后才能得到可以添加到事件文件中的数据。
262 |
263 | 完整创建标量摘要的代码如下:
264 |
265 | ```python
266 | import tensorflow as tf
267 |
268 | with tf.Graph().as_default() as graph:
269 | var = tf.Variable(0., name='var')
270 | summary_op = tf.summary.scalar('value', var) # 给变量var创建摘要
271 |
272 | random_val = tf.random_normal([], mean=0.5, stddev=1.)
273 | assign_op = var.assign_add(random_val) # var累加一个随机数
274 |
275 | with tf.Session(graph=graph) as sess:
276 | # 创建一个事件文件管理对象
277 | writer = tf.summary.FileWriter('./graphs')
278 | writer.add_graph(graph)
279 |
280 | sess.run(tf.global_variables_initializer())
281 | for i in range(100):
282 | # 得到summary_op的值
283 | summary, _ = sess.run([summary_op, assign_op])
284 | # summary_op的值写入事件文件
285 | writer.add_summary(summary, i)
286 |
287 | writer.close()
288 | ```
289 |
290 | 上述代码执行之后,在SCALARS面板中显示如下:
291 |
292 |
293 |
294 | 面板的右侧,可以看到一个曲线图,这个曲线图是我们创建的变量`var`的值的变化的曲线。纵坐标是我们设置的`summary_op`的`name`。曲线图可以放大、缩小,也可以下载。
295 |
296 | 面板左侧可以调整右侧的显示内容。我们看到的右侧的线是一个曲线,然而我们得到应该是一个折线图,这是因为左侧设置了平滑度(Smoothing)。横坐标默认是步数(STEP),也可以设置为按照相对值(RELATIVE)或按照时间顺序(WALL)显示。
297 |
298 | SCALARS面板也可以同时显示多个标量的摘要。
299 |
300 | ### 3.3 HISTOGRAMS
301 |
302 | HISTOGRAMS面板显示的是直方图,对于2维、3维等数据,使用直方图可以更好的展示出其规律。例如
303 |
304 | ```python
305 | import tensorflow as tf
306 |
307 | with tf.Graph().as_default() as graph:
308 | var = tf.Variable(tf.random_normal([200, 10], mean=0., stddev=1.), name='var')
309 | histogram = tf.summary.histogram('histogram', var)
310 |
311 | with tf.Session(graph=graph) as sess:
312 | writer = tf.summary.FileWriter('./graphs')
313 | writer.add_graph(graph)
314 |
315 | sess.run(tf.global_variables_initializer())
316 | for i in range(10):
317 | summaries, _ = sess.run([histogram, var])
318 | writer.add_summary(summaries, i)
319 |
320 | writer.close()
321 | ```
322 |
323 | 上述代码,我们使用了`tf.summary.histogram()`方法建立了柱状图摘要,其使用方法类似于scalar摘要。在HISTOGRAMS面板中显示如下。
324 |
325 | 
326 |
327 |
328 |
329 | ### 3.4 DISTRIBUTIONS
330 |
331 | DISTRIBUTIONS面板与HISTOGRAMS面板相关联,当我们创建了柱状图摘要之后,在DISTRIBUTIONS面板也可以看到图像,上述HISTOGRAMS面板对应的在DISTRIBUTIONS面板中显示如下:
332 |
333 | 
334 |
335 | DISTRIBUTIONS面板可以看做HISTOGRAMS面板的压缩后的图像。
336 |
337 | ### 3.5 IMAGES
338 |
339 | IMAGES面板用来展示训练过程中的图像,例如我们需要对比原始图像与算法处理过的图像的差异时,我们可以使用IMAGES面板将其展示出来。
340 |
341 | 建立IMAGES摘要的函数是:`tf.summary.image()`,具体如下:
342 |
343 | ```python
344 | tf.summary.image(name, tensor, max_outputs=3, collections=None)
345 | ```
346 |
347 | 这里的参数`tensor`必须是4D的shape=`[batch_size, height, width, channels]`的图片。参数`max_outputs`代表从一个批次中摘要几个图片。
348 |
349 | 举例,我们使用随机数初始化10张rgba的图片,并选取其中的前5张作为摘要:
350 |
351 | ```python
352 | import tensorflow as tf
353 |
354 | with tf.Graph().as_default() as graph:
355 | var = tf.Variable(tf.random_normal([10, 28, 28, 4], mean=0., stddev=1.), name='var')
356 | image = tf.summary.image('image', var, 5)
357 |
358 | with tf.Session(graph=graph) as sess:
359 | writer = tf.summary.FileWriter('./graphs')
360 | writer.add_graph(graph)
361 |
362 | sess.run(tf.global_variables_initializer())
363 | summaries, _ = sess.run([image, var])
364 | writer.add_summary(summaries)
365 |
366 | writer.close()
367 | ```
368 |
369 | 在IMAGES面板中显示如下:
370 |
371 | 
372 |
373 |
374 |
375 | ### 3.6 AUDIO
376 |
377 | AUDIO面板与IMAGES面板类似,只不过是将图像换成了音频。音频数据比图像数据少一个维度。创建AUDIO摘要的方法是:`tf.summary.audio()`,具体如下:
378 |
379 | ```python
380 | tf.summary.audio(name, tensor, sample_rate, max_outputs=3, collections=None)
381 | ```
382 |
383 | 这里的参数`tensor`必须是3D的shape=`[batch_size, frames, channels]`或2D的shape=`[batch_size, frames]`的音频。`sample_rate`参数。参数`max_outputs`代表从一个批次中摘要几个音频。
384 |
385 | 下面我们生成10段采样率为22050的长度为2秒的噪声,并选取其中的5个进行采样:
386 |
387 | ```python
388 | import tensorflow as tf
389 |
390 | with tf.Graph().as_default() as graph:
391 | var = tf.Variable(tf.random_normal([10, 44100, 2], mean=0., stddev=1.), name='var')
392 | audio = tf.summary.audio('video', var, 22050, 5)
393 |
394 | with tf.Session(graph=graph) as sess:
395 | writer = tf.summary.FileWriter('./graphs')
396 | writer.add_graph(graph)
397 |
398 | sess.run(tf.global_variables_initializer())
399 | summaries, _ = sess.run([audio, var])
400 | writer.add_summary(summaries)
401 |
402 | writer.close()
403 | ```
404 |
405 | 在AUDIO面板中可以播放、下载采样的音频,显示效果如下:
406 |
407 | 
408 |
409 |
410 |
411 | ### 3.7 TEXT
412 |
413 | TEXT面板用来可视化多行字符串,使用`tf.summary.text()`方法进行。
414 |
415 | ```python
416 | tf.summary.text(name, tensor, collections=None)
417 | ```
418 |
419 | 举例:
420 |
421 | ```python
422 | import tensorflow as tf
423 |
424 | with tf.Graph().as_default() as graph:
425 | var = tf.constant(['Tensorflow 基础教程做的挺不错 中午加肥肉',
426 | 'Tensorboard 做的也很棒',
427 | '你是一个全能型选手'])
428 | text = tf.summary.text('text', var)
429 |
430 | with tf.Session(graph=graph) as sess:
431 | writer = tf.summary.FileWriter('./graphs')
432 | writer.add_graph(graph)
433 |
434 | sess.run(tf.global_variables_initializer())
435 | summaries, _ = sess.run([text, var])
436 | writer.add_summary(summaries)
437 |
438 | writer.close()
439 | ```
440 |
441 | 可视化效果如下:
442 |
443 | 
444 |
445 |
446 |
447 | ### 3.8 EMBEDDINGS
448 |
449 | EMBEDDINGS面板一般用来可视化词向量(word embedding)。在自然语言处理、推荐系统中较为常见。embeding的可视化与其它类型的可视化完全不同,需要用到Tensoroard插件并配合检查点操作。具体教程我们在之后的内容中详述,这里,我们可以查看一下手写数字可视化后的效果:
450 |
451 | 
--------------------------------------------------------------------------------
/ch11/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/1.png
--------------------------------------------------------------------------------
/ch11/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/2.png
--------------------------------------------------------------------------------
/ch11/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/3.png
--------------------------------------------------------------------------------
/ch11/images/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/audio.png
--------------------------------------------------------------------------------
/ch11/images/connected_series.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/connected_series.png
--------------------------------------------------------------------------------
/ch11/images/contral_dependency.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/contral_dependency.png
--------------------------------------------------------------------------------
/ch11/images/dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/dataflow.png
--------------------------------------------------------------------------------
/ch11/images/distributions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/distributions.png
--------------------------------------------------------------------------------
/ch11/images/embeddings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/embeddings.png
--------------------------------------------------------------------------------
/ch11/images/graph_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/graph_1.png
--------------------------------------------------------------------------------
/ch11/images/graph_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/graph_2.png
--------------------------------------------------------------------------------
/ch11/images/graph_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/graph_icon.png
--------------------------------------------------------------------------------
/ch11/images/histograms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/histograms.png
--------------------------------------------------------------------------------
/ch11/images/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/image.png
--------------------------------------------------------------------------------
/ch11/images/scalars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/scalars.png
--------------------------------------------------------------------------------
/ch11/images/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/text.png
--------------------------------------------------------------------------------
/ch11/images/unconnected_series.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/ch11/images/unconnected_series.png
--------------------------------------------------------------------------------
/exper/01.graph and session.py:
--------------------------------------------------------------------------------
1 | """
2 | 本例子中写了两个图。
3 | g1 描述了两个矩阵的乘法,其中乘法的部分在GPU上执行。
4 | g2 描述了两个子图构成的一个图
5 | """
6 |
7 | import tensorflow as tf
8 |
9 |
10 | # 获取tensorflow中的默认图
11 | g1 = tf.get_default_graph()
12 | with g1.as_default():
13 | with tf.device('/cpu:0'):
14 | a = tf.constant([[1, 2, 3]])
15 | a = a + a
16 | b = tf.constant([[3], [2], [1]])
17 | b = b + b
18 | with tf.device('/gpu:0'):
19 | c = tf.matmul(a, b)
20 |
21 | config = tf.ConfigProto(
22 | log_device_placement=True,
23 | allow_soft_placement=True)
24 | with tf.Session(graph=g1, config=config) as sess:
25 | print(sess.run([c]))
26 | print(sess.run([c], feed_dict={a: [[1, 1, 1]]}))
27 |
28 |
29 | g2 = tf.Graph()
30 | with g2.as_default():
31 | x = tf.constant(1)
32 | x = tf.add(x, tf.constant(2))
33 | x = tf.multiply(x, tf.constant(5))
34 |
35 | y1 = tf.constant(3)
36 | y2 = tf.constant(1)
37 | y = y1 + y2
38 | y = x % y
39 | y = x // y
40 |
41 | with tf.Session(graph=g2) as sess:
42 | print(sess.run(y))
43 |
44 |
--------------------------------------------------------------------------------
/exper/02.control_dependencies.py:
--------------------------------------------------------------------------------
1 | import tensorflow as tf
2 |
3 |
4 | g = tf.get_default_graph()
5 | with g.as_default():
6 | a = tf.constant([[1.0, 2.0, 3.0]], name='a')
7 | b = tf.constant([[3.0], [4.0], [5.0]], name='b')
8 | c = tf.matmul(a, b)
9 | # tf.Print可以查看数据流过c时对应的a、b、c的值
10 | c = tf.Print(c, [a, b, c], 'a、b、c is :')
11 | c = tf.add(c, tf.constant(5.0))
12 | c = tf.Print(c, [a, b, c], 'a、b、c is :')
13 | with g.control_dependencies([c]):
14 | no_op = tf.no_op()
15 |
16 | with tf.Session(graph=g) as sess:
17 | sess.run(no_op)
--------------------------------------------------------------------------------
/exper/02.tensor slice.py:
--------------------------------------------------------------------------------
1 | """
2 | 本例用以强化张量阶、形状、维度等概念
3 | 以及掌握张量切片的方法
4 | """
5 | import tensorflow as tf
6 |
7 |
8 | # img代表用1填充的shape为[10, 28, 28, 3]的张量
9 | # 可以理解为10张大小为28*28像素的jpg图片
10 | img = tf.ones(shape=[10, 28, 28, 3])
11 |
12 |
13 | # img的阶是多少?
14 | img_rank = tf.rank(img)
15 |
16 |
17 | # tensor切片之后的shape是多少?
18 | print(img[:].shape)
19 | print(img[5:].shape) # 从第5开始包括第5个到末尾
20 | print(img[:6].shape) # 从第0开始到第6个,不靠看第6个
21 | print(img[0:1].shape)
22 | print(img[5:10:2].shape) # 步长为2
23 |
24 |
25 | # tensor索引
26 | print(img[0].shape)
27 | print(img[0, 1].shape)
28 | print(img[0, 1, 7].shape)
29 |
30 | # 切片与索引混用
31 | print(img[5, 3:7:2].shape)
32 | print(img[:, 2].shape) # 第一个维度上不变,第二个维度上选取第2个元素
33 | print(img[:, 2, 5:10].shape)
34 |
35 |
36 | # 再次索引与切片
37 | tmp = img[5]
38 | tmp = tmp[3]
39 | assert img[5][3].shape == tmp.shape
40 |
41 | print(img[:, 3][5])
42 |
43 |
44 | # 使用tf.slice进行切片也很方便
45 | res1 = tf.slice(img, [1, 0, 0, 0], [1, 28, 28, 1])
46 |
47 | res2 = tf.slice(img, [5, 0, 0, 0], [1, 28, 28, 1])
48 |
49 | # 连接的时候连接的维度可以有不同的长度,但其它维度必须相同
50 | res = tf.concat([res1, res2], 0)
51 | res = tf.concat([res1, res2], 1)
52 | res = tf.concat([res1, res2], 2)
53 | res = tf.concat([res1, res2], 3)
--------------------------------------------------------------------------------
/exper/03.knn.py:
--------------------------------------------------------------------------------
1 | import tensorflow as tf
2 | import numpy as np
3 | from tensorflow.examples.tutorials.mnist import input_data
4 |
5 | mnist = input_data.read_data_sets("/tmp/MNIST_data", one_hot=True)
6 |
7 | k = 7
8 | test_num = 5000
9 |
10 | train_images, train_labels = mnist.train.next_batch(55000)
11 | test_images, test_labels = mnist.test.next_batch(test_num)
12 |
13 | x_train = tf.placeholder(tf.float32)
14 | x_test = tf.placeholder(tf.float32)
15 | y_train = tf.placeholder(tf.float32)
16 |
17 | # 欧式距离
18 | euclidean_distance = tf.sqrt(tf.reduce_sum(tf.square(x_train - x_test), 1))
19 | # 计算最相近的k个样本的索引
20 | _, nearest_index = tf.nn.top_k(-euclidean_distance, k)
21 |
22 | with tf.Session() as sess:
23 | sess.run(tf.global_variables_initializer())
24 |
25 | predicted_num = 0
26 | # 对每个图片进行预测
27 | for i in range(test_images.shape[0]):
28 | # 最近k个样本的标记索引
29 | nearest_index_res = sess.run(
30 | nearest_index,
31 | feed_dict={
32 | x_train: train_images,
33 | y_train: train_labels,
34 | x_test: test_images[i]})
35 | # 最近k个样本的标记
36 | nearest_label = []
37 | for j in range(k):
38 | nearest_label.append(list(train_labels[nearest_index_res[j]]))
39 |
40 | predicted_class = sess.run(tf.argmax(tf.reduce_sum(nearest_label, 0), 0))
41 | true_class = sess.run(tf.argmax(test_labels[i]))
42 | if predicted_class == true_class:
43 | predicted_num += 1
44 |
45 | if i % 100 == 0:
46 | print('step is %d accuracy is %.4f' % (i, predicted_num / (i+1)))
47 | print('accuracy is %.4f' % predicted_num / test_num)
--------------------------------------------------------------------------------
/images/AnimatedFileQueues.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/AnimatedFileQueues.gif
--------------------------------------------------------------------------------
/images/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/graph.png
--------------------------------------------------------------------------------
/images/graph_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/graph_add.png
--------------------------------------------------------------------------------
/images/graph_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/graph_more.png
--------------------------------------------------------------------------------
/images/sub_graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/sub_graph.png
--------------------------------------------------------------------------------
/images/thchs30-train-wav-v2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edu2act/course-tensorflow/8a4ae1cc4c786f638477b1d6aeda769ce38120df/images/thchs30-train-wav-v2.jpg
--------------------------------------------------------------------------------