提交 5c73a18d 编写于 作者: W wizardforcel

ch12 style

上级 37ef1f6e
......@@ -62,7 +62,7 @@ OReilly Hands On Machine Learning with Scikit Learn and TensorFlow
| 九、启动并运行TensorFlow | [@akonwang](https://github.com/wangxupeng) [@WilsonQu](https://github.com/WilsonQu) | [@Lisanaaa](https://github.com/Lisanaaa) [@飞龙](https://github.com/wizardforcel) |
| 十、人工神经网络介绍 | [@akonwang](https://github.com/wangxupeng) [@friedhelm739](https://github.com/friedhelm739) | [@飞龙](https://github.com/wizardforcel) |
| 十一、训练深层神经网络 | [@akonwang](https://github.com/wangxupeng) [@飞龙](https://github.com/wizardforcel) | [@飞龙](https://github.com/wizardforcel) [@Zeyu Zhong](https://github.com/zhearing) |
| 十二、设备和服务器上的分布式TensorFlow | [*@空白*](https://github.com/yhcheer) |
| 十二、设备和服务器上的分布式TensorFlow | [*@空白*](https://github.com/yhcheer) | [@飞龙](https://github.com/wizardforcel) |
| 十三、卷积神经网络 | [@akonwang](https://github.com/wangxupeng) [@WilsonQu](https://github.com/WilsonQu) | [@飞龙](https://github.com/wizardforcel) |
| 十四、循环神经网络 | [@akonwang](https://github.com/wangxupeng) [@alexcheen](https://github.com/alexcheen) [@飞龙](https://github.com/wizardforcel) | [@飞龙](https://github.com/wizardforcel) |
| 十五、自编码器 | [@akonwang](https://github.com/wangxupeng) | [@飞龙](https://github.com/wizardforcel) |
......
# 第12章 设备和服务器上的分布式TensorFlow
在第11章,我们讨论了几种可以明显加速训练的技术:更好的权重初始化,批量标准化,复杂的优化器等等。 但是,即使采用了所有这些技术,在具有单个CPU的单台机器上训练大型神经网络可能需要几天甚至几周的时间。
在第 11 章,我们讨论了几种可以明显加速训练的技术:更好的权重初始化,批量标准化,复杂的优化器等等。 但是,即使采用了所有这些技术,在具有单个 CPU 的单台机器上训练大型神经网络可能需要几天甚至几周的时间。
在本章中,我们将看到如何使用TensorFlow在多个设备(CPU和GPU)上分配计算并将它们并行运行(参见图12-1)。 首先,我们会先在一台机器上的多个设备上分配计算,然后在多台机器上的多个设备上分配计算。
在本章中,我们将看到如何使用 TensorFlow 在多个设备(CPU 和 GPU)上分配计算并将它们并行运行(参见图 12-1)。 首先,我们会先在一台机器上的多个设备上分配计算,然后在多台机器上的多个设备上分配计算。
![1524821340342](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/1.png?raw=true)
与其他神经网络框架相比,TensorFlow对分布式计算的支持是其主要亮点之一。 它使您可以完全控制如何跨设备和服务器分布(或复制)您的计算图,并且可以让您以灵活的方式并行和同步操作,以便您可以在各种并行方法之间进行选择。
与其他神经网络框架相比,TensorFlow 对分布式计算的支持是其主要亮点之一。 它使您可以完全控制如何跨设备和服务器分布(或复制)您的计算图,并且可以让您以灵活的方式并行和同步操作,以便您可以在各种并行方法之间进行选择。
我们来看一些最流行的方法来并行执行和训练一个神经网络,这让我们不再需要等待数周才能完成训练算法,而最终可能只会等待几个小时。 这不仅可以节省大量时间,还意味着您可以更轻松地尝试各种模型,并经常重新训练模型上的新数据。
还有其他很好的并行化例子,包括当我们在微调模型时可以探索更大的超参数空间,并有效地运行大规模神经网络。
但我们必须先学会走路才能跑步。 我们先从一台机器上的几个GPU上并行化简单图形开始。
但我们必须先学会走路才能跑步。 我们先从一台机器上的几个 GPU 上并行化简单图形开始。
## 一台机器上多设备
只需添加GPU卡到单个机器,您就可以获得主要的性能提升。 事实上,在很多情况下,这就足够了。 你根本不需要使用多台机器。 例如,通常在单台机器上使用8个GPU,而不是在多台机器上使用16个GPU(由于多机器设置中的网络通信带来的额外延迟),可以同样快地训练神经网络。
只需添加 GPU 卡到单个机器,您就可以获得主要的性能提升。 事实上,在很多情况下,这就足够了。 你根本不需要使用多台机器。 例如,通常在单台机器上使用 8 个 GPU,而不是在多台机器上使用 16 个 GPU(由于多机器设置中的网络通信带来的额外延迟),可以同样快地训练神经网络。
在本节中,我们将介绍如何设置您的环境,以便TensorFlow可以在一台机器上使用多个GPU卡。 然后,我们将看看如何在可用设备上进行分布操作,并且并行执行它们。
在本节中,我们将介绍如何设置您的环境,以便 TensorFlow 可以在一台机器上使用多个 GPU 卡。 然后,我们将看看如何在可用设备上进行分布操作,并且并行执行它们。
### 安装
为了在多个GPU卡上运行TensorFlow,首先需要确保GPU卡具有NVidia计算能力(大于或等于3.0)。 这包括Nvidia的Titan,Titan X,K20和K40(如果你拥有另一张卡,你可以在https://developer.nvidia.com/cuda-gpus查看它的兼容性)。
为了在多个 GPU 卡上运行 TensorFlow,首先需要确保 GPU 卡具有 NVidia 计算能力(大于或等于3.0)。 这包括 Nvidia 的 Titan,Titan X,K20 和 K40(如果你拥有另一张卡,你可以在 <https://developer.nvidia.com/cuda-gpus> 查看它的兼容性)。
如果您不拥有任何GPU卡,则可以使用具有GPU功能的主机服务器,如Amazon AWS。 在ŽigaAvsec的[博客文章](https://goo.gl/kbge5b)中,提供了在Amazon AWS GPU实例上使用Python 3.5设置TensorFlow 0.9的详细说明。将它更新到最新版本的TensorFlow应该不会太难。 Google还发布了一项名为Cloud Machine Learning的云服务来运行TensorFlow图表。 2016年5月,他们宣布他们的平台现在包括配备 tensor processing units(TPU)的服务器,专门用于机器学习的处理器,比许多GPU处理ML任务要快得多。 当然,另一种选择只是购买你自己的GPU卡。 Tim Dettmers写了一篇很棒的博客文章来帮助你选择,他会定期更新它。
如果您不拥有任何 GPU 卡,则可以使用具有 GPU 功能的主机服务器,如 Amazon AWS。 在 ŽigaAvsec 的[博客文章](https://goo.gl/kbge5b)中,提供了在 Amazon AWS GPU 实例上使用 Python 3.5 设置 TensorFlow 0.9 的详细说明。将它更新到最新版本的 TensorFlow 应该不会太难。 Google 还发布了一项名为 Cloud Machine Learning 的云服务来运行 TensorFlow 图表。 2016 年 5 月,他们宣布他们的平台现在包括配备张量处理器(TPU)的服务器,专门用于机器学习的处理器,比许多 GPU 处理 ML 任务要快得多。 当然,另一种选择只是购买你自己的 GPU 卡。 Tim Dettmers 写了一篇很棒的博客文章来帮助你选择,他会定期更新它。
您必须下载并安装相应版本的CUDA和cuDNN库(如果您使用的是TensorFlow 1.0.0,则为CUDA 8.0和cuDNN 5.1),并设置一些环境变量,以便TensorFlow知道在哪里可以找到CUDA和cuDNN。 详细的安装说明可能会相当迅速地更改,因此最好按照TensorFlow网站上的说明进行操作。
您必须下载并安装相应版本的 CUDA 和 cuDNN 库(如果您使用的是 TensorFlow 1.0.0,则为 CUDA 8.0 和 cuDNN 5.1),并设置一些环境变量,以便 TensorFlow 知道在哪里可以找到 CUDA 和 cuDNN。 详细的安装说明可能会相当迅速地更改,因此最好按照 TensorFlow 网站上的说明进行操作。
Nvidia的CUDA允许开发者使用支持CUDA的GPU进行各种计算(不仅仅是图形加速)。 Nvidia的CUDA深度神经网络库(cuDNN)是针对DNN的GPU加速原语库。 它提供了常用DNN计算的优化实现,例如激活层,归一化,前向和后向卷积以及池化(参见第13章)。 它是Nvidia Deep Learning SDK的一部分(请注意,它需要创建一个Nvidia开发者帐户才能下载它)。 TensorFlow使用CUDA和cuDNN来控制GPU卡并加速计算(见图12-2)。
Nvidia 的 CUDA 允许开发者使用支持 CUDA 的 GPU 进行各种计算(不仅仅是图形加速)。 Nvidia 的 CUDA 深度神经网络库(cuDNN)是针对 DNN 的 GPU 加速原语库。 它提供了常用 DNN 计算的优化实现,例如激活层,归一化,前向和后向卷积以及池化(参见第 13 章)。 它是 Nvidia Deep Learning SDK 的一部分(请注意,它需要创建一个 Nvidia 开发者帐户才能下载它)。 TensorFlow 使用 CUDA 和 cuDNN 来控制 GPU 卡并加速计算(见图 12-2)。
![1524830422813](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/2.png?raw=true)
您可以使用`nvidia-smi`命令来检查CUDA是否已正确安装。 它列出了可用的GPU卡以及每张卡上运行的进程:
您可以使用`nvidia-smi`命令来检查 CUDA 是否已正确安装。 它列出了可用的 GPU 卡以及每张卡上运行的进程:
![1524830581469](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/2-2.png?raw=true)
最后,您必须安装支持GPU的TensorFlow。 如果你使用`virtualenv`创建了一个独立的环境,你首先需要激活它:
最后,您必须安装支持 GPU 的 TensorFlow。 如果你使用`virtualenv`创建了一个独立的环境,你首先需要激活它:
```
$ cd $ML_PATH
# Your ML working directory (e.g., $HOME/ml) $ source env/bin/activate
```
然后安装合适的支持GPU的TensorFlow版本:
然后安装合适的支持 GPU 的 TensorFlow 版本:
```
$ pip3 install --upgrade tensorflow-gpu
```
现在您可以打开一个Python shell并通过导入TensorFlow并创建一个会话来检查TensorFlow是否正确检测并使用CUDA和cuDNN:
现在您可以打开一个 Python shell 并通过导入 TensorFlow 并创建一个会话来检查 TensorFlow 是否正确检测并使用 CUDA 和 cuDNN:
```
>>> import tensorflow as tf
......@@ -75,17 +75,17 @@ I [...]/gpu_device.cc:839] Creating TensorFlow device
(/gpu:0) -> (device: 0, name: GRID K520, pci bus id: 0000:00:03.0)
```
看起来不错! TensorFlow检测到CUDA和cuDNN库,并使用CUDA库来检测GPU卡(在这种情况下是Nvidia Grid K520卡)。
看起来不错!TensorFlow 检测到 CUDA 和 cuDNN 库,并使用 CUDA 库来检测 GPU 卡(在这种情况下是 Nvidia Grid K520 卡)。
## 管理GPU内存
## 管理 GPU 内存
默认情况下,TensorFlow会在您第一次运行图形时自动获取所有可用GPU中的所有RAM,因此当第一个程序仍在运行时,您将无法启动第二个TensorFlow程序。 如果你尝试,你会得到以下错误:
默认情况下,TensorFlow 会在您第一次运行图形时自动获取所有可用 GPU 中的所有 RAM,因此当第一个程序仍在运行时,您将无法启动第二个 TensorFlow 程序。 如果你尝试,你会得到以下错误:
```
E [...]/cuda_driver.cc:965] failed to allocate 3.66G (3928915968 bytes) from device: CUDA_ERROR_OUT_OF_MEMORY
```
一种解决方案是在不同的GPU卡上运行每个进程。 为此,最简单的选择是设置`CUDA_VISIBLE_DEVICES`环境变量,以便每个进程只能看到对应的GPU卡。 例如,你可以像这样启动两个程序:
一种解决方案是在不同的 GPU 卡上运行每个进程。 为此,最简单的选择是设置`CUDA_VISIBLE_DEVICES`环境变量,以便每个进程只能看到对应的 GPU 卡。 例如,你可以像这样启动两个程序:
```
$ CUDA_VISIBLE_DEVICES=0,1 python3 program_1.py
......@@ -93,11 +93,11 @@ $ CUDA_VISIBLE_DEVICES=0,1 python3 program_1.py
$ CUDA_VISIBLE_DEVICES=3,2 python3 program_2.py
```
程序#1只会看到GPU卡0和1(分别编号为0和1),程序#2只会看到GPU卡2和3(分别编号为1和0)。 一切都会正常工作(见图12-3)。
程序 #1 只会看到 GPU 卡 0 和 1(分别编号为 0 和 1),程序 #2 只会看到 GPU 卡 2 和 3(分别编号为 1 和 0)。 一切都会正常工作(见图 12-3)。
![1524831933410](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/3.png?raw=true)
另一种选择是告诉TensorFlow只抓取一小部分内存。 例如,要使TensorFlow只占用每个GPU内存的40%,您必须创建一个ConfigProto对象,将其`gpu_options.per_process_gpu_memory_fraction`选项设置为0.4,并使用以下配置创建session
另一种选择是告诉 TensorFlow 只抓取一小部分内存。 例如,要使 TensorFlow 只占用每个 GPU 内存的 40%,您必须创建一个`ConfigProto`对象,将其`gpu_options.per_process_gpu_memory_fraction`选项设置为 0.4,并使用以下配置创建`session`
```
config = tf.ConfigProto()
......@@ -105,47 +105,47 @@ config.gpu_options.per_process_gpu_memory_fraction = 0.4
session = tf.Session(config=config)
```
现在像这样的两个程序可以使用相同的GPU卡并行运行(但不是三个,因为3×0.4> 1)。 见图12-4。
现在像这样的两个程序可以使用相同的 GPU 卡并行运行(但不是三个,因为`3×0.4> 1`)。 见图 12-4。
![1524832121806](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/4.png?raw=true)
如果在两个程序都运行时运行`nvidia-smi`命令,则应该看到每个进程占用每个卡的总RAM大约40%
如果在两个程序都运行时运行`nvidia-smi`命令,则应该看到每个进程占用每个卡的总 RAM 大约 40%
![1524832269888](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/4-2.png?raw=true)
另一种选择是告诉TensorFlow只在需要时才抓取内存。 为此,您必须将`config.gpu_options.allow_growth`设置为True。 但是,TensorFlow一旦抓取内存就不会释放内存(以避免内存碎片),因此您可能会在一段时间后内存不足。 是否使用此选项可能难以确定,因此一般而言,您可能想要坚持之前的某个选项。
另一种选择是告诉 TensorFlow 只在需要时才抓取内存。 为此,您必须将`config.gpu_options.allow_growth`设置为`True`。但是,TensorFlow 一旦抓取内存就不会释放内存(以避免内存碎片),因此您可能会在一段时间后内存不足。 是否使用此选项可能难以确定,因此一般而言,您可能想要坚持之前的某个选项。
好的,现在你已经有了一个支持GPU的TensorFlow安装。 让我们看看如何使用它!
好的,现在你已经有了一个支持 GPU 的 TensorFlow 安装。 让我们看看如何使用它!
## 设备布置操作
TensorFlow白皮书介绍了一种友好的动态布置器算法,该算法能够自动将操作分布到所有可用设备上,并考虑到以前运行图中所测量的计算时间,估算每次操作的输入和输出张量的大小, 每个设备可用的RAM,传输数据进出设备时的通信延迟,来自用户的提示和约束等等。 不幸的是,这种复杂的算法是谷歌内部的,它并没有在TensorFlow的开源版本中发布。它被排除在外的原因似乎是,由用户指定的一小部分放置规则实际上比动态放置器放置的更有效。 然而,TensorFlow团队正在努力改进它,并且最终可能会被开放。
TensorFlow 白皮书介绍了一种友好的动态布置器算法,该算法能够自动将操作分布到所有可用设备上,并考虑到以前运行图中所测量的计算时间,估算每次操作的输入和输出张量的大小, 每个设备可用的 RAM,传输数据进出设备时的通信延迟,来自用户的提示和约束等等。 不幸的是,这种复杂的算法是谷歌内部的,它并没有在 TensorFlow 的开源版本中发布。它被排除在外的原因似乎是,由用户指定的一小部分放置规则实际上比动态放置器放置的更有效。 然而,TensorFlow 团队正在努力改进它,并且最终可能会被开放。
在此之前,TensorFlow都是简单的放置,它(如其名称所示)非常基本。
### 简单放置
无论何时运行图形,如果TensorFlow需要评估尚未放置在设备上的节点,则它会使用简单放置器将其放置在未放置的所有其他节点上。 简单放置尊重以下规则:
无论何时运行图形,如果 TensorFlow 需要求值尚未放置在设备上的节点,则它会使用简单放置器将其放置在未放置的所有其他节点上。 简单放置尊重以下规则:
- 如果某个节点已经放置在图形的上一次运行中的某个设备上,则该节点将保留在该设备上。
- 否则,如果用户将一个节点固定到设备上(下面介绍),则放置器将其放置在该设备上。
- 否则,它默认为GPU#0,如果没有GPU,则默认为CPU。
- 否则,它默认为 GPU#0,如果没有 GPU,则默认为 CPU。
正如您所看到的,将操作放在适当的设备上主要取决于您。 如果您不做任何事情,整个图表将被放置在默认设备上。 要将节点固定到设备上,您必须使用device()函数创建一个设备块。 例如,以下代码将变量a和常量b固定在CPU上,但乘法节点c不固定在任何设备上,因此将放置在默认设备上:
正如您所看到的,将操作放在适当的设备上主要取决于您。 如果您不做任何事情,整个图表将被放置在默认设备上。 要将节点固定到设备上,您必须使用`device()`函数创建一个设备块。 例如,以下代码将变量`a`和常量`b`固定在 CPU 上,但乘法节点`c`不固定在任何设备上,因此将放置在默认设备上:
```
with tf.device("/cpu:0"):
a = tf.Variable(3.0)
b = tf.constant(4.0)
a = tf.Variable(3.0)
b = tf.constant(4.0)
c = a * b
```
其中,“/ cpu:0”设备合计多CPU系统上的所有CPU。 目前没有办法在特定CPU上固定节点或仅使用所有CPU的子集。
其中,`"/cpu:0"`设备合计多 CPU 系统上的所有 CPU。 目前没有办法在特定 CPU 上固定节点或仅使用所有 CPU 的子集。
### 记录放置位置
让我们检查一下简单的放置器是否遵守我们刚刚定义的布局约束条件。 为此,您可以将log_device_placement选项设置为True; 这告诉放置器在放置节点时记录消息。 例如:
让我们检查一下简单的放置器是否遵守我们刚刚定义的布局约束条件。 为此,您可以将`log_device_placement`选项设置为`True`;这告诉放置器在放置节点时记录消息。例如:
```
>>> config = tf.ConfigProto()
......@@ -164,30 +164,30 @@ I [...] a/initial_value: /job:localhost/replica:0/task:0/cpu:0
12
```
Info中以大写字母“I”开头的行是日志消息。 当我们创建一个会话时,TensorFlow会记录一条消息,告诉我们它已经找到了一个GPU卡(在这个例子中是Grid K520卡)。 然后,我们第一次运行图形(在这种情况下,当初始化变量a时),简单布局器运行,并将每个节点放置在分配给它的设备上。正如预期的那样,日志消息显示所有节点都放在“/ cpu:0”上,除了乘法节点,它以默认设备“/ gpu:0”结束(您可以先忽略前缀:`/job:localhost /replica:0 /task:0;` 我们将在一会儿讨论它)。 注意,我们第二次运行图(计算c)时,由于TensorFlow需要计算的所有节点c都已经放置,所以不使用布局器。
`Info`中以大写字母`I`开头的行是日志消息。 当我们创建一个会话时,TensorFlow 会记录一条消息,告诉我们它已经找到了一个 GPU 卡(在这个例子中是 Grid K520 卡)。 然后,我们第一次运行图形(在这种情况下,当初始化变量`a`时),简单布局器运行,并将每个节点放置在分配给它的设备上。正如预期的那样,日志消息显示所有节点都放在`"/cpu:0"`上,除了乘法节点,它以默认设备`"/gpu:0"`结束(您可以先忽略前缀:`/job:localhost/replica:0/task:0;`我们将在一会儿讨论它)。 注意,我们第二次运行图(计算`c`)时,由于 TensorFlow 需要计算的所有节点`c`都已经放置,所以不使用布局器。
### 动态放置功能
创建设备块时,可以指定一个函数,而不是设备名称。 TensorFlow会调用这个函数来进行每个需要放置在设备块中的操作,并且该函数必须返回设备的名称来固定操作。 例如,以下代码将固定所有变量节点到“/ cpu:0”(在本例中只是变量a)和所有其他节点到“/ gpu:0”
创建设备块时,可以指定一个函数,而不是设备名称。TensorFlow 会调用这个函数来进行每个需要放置在设备块中的操作,并且该函数必须返回设备的名称来固定操作。 例如,以下代码将固定所有变量节点到`"/cpu:0"`(在本例中只是变量`a`)和所有其他节点到`"/gpu:0"`
```
def variables_on_cpu(op):
if op.type == "Variable":
return "/cpu:0"
else:
return "/gpu:0"
if op.type == "Variable":
return "/cpu:0"
else:
return "/gpu:0"
with tf.device(variables_on_cpu):
a = tf.Variable(3.0)
b = tf.constant(4.0)
c = a * b
a = tf.Variable(3.0)
b = tf.constant(4.0)
c = a * b
```
您可以轻松实现更复杂的算法,例如以循环方式用GPU锁定变量。
### 操作和内核
对于在设备上运行的TensorFlow操作,它需要具有该设备的实现;这被称为内核。 许多操作对于CPU和GPU都有内核,但并非全部都是。 例如,TensorFlow没有用于整数变量的GPU内核,因此当TensorFlow尝试将变量i放置到GPU#0时,以下代码将失败:
对于在设备上运行的 TensorFlow 操作,它需要具有该设备的实现;这被称为内核。 许多操作对于 CPU 和 GPU 都有内核,但并非全部都是。 例如,TensorFlow 没有用于整数变量的 GPU 内核,因此当 TensorFlow 尝试将变量i放置到 GPU#0 时,以下代码将失败:
```
>>> with tf.device("/gpu:0"):
......@@ -199,56 +199,56 @@ Traceback (most recent call last):
tensorflow.python.framework.errors.InvalidArgumentError: Cannot assign a device to node 'Variable': Could not satisfy explicit device specification
```
请注意,TensorFlow推断变量必须是int32类型,因为初始化值是一个整数。 如果将初始化值更改为3.0而不是3,或者如果在创建变量时显式设置`dtype = tf.float32`,则一切正常。
请注意,TensorFlow 推断变量必须是`int32`类型,因为初始化值是一个整数。 如果将初始化值更改为 3.0 而不是 3,或者如果在创建变量时显式设置`dtype = tf.float32`,则一切正常。
### 软放置
默认情况下,如果您尝试在操作没有内核的设备上固定操作,则当TensorFlow尝试将操作放置在设备上时,您会看到前面显示的异常。 如果您更喜欢TensorFlow回退到CPU,则可以将`allow_soft_placement`配置选项设置为True
默认情况下,如果您尝试在操作没有内核的设备上固定操作,则当 TensorFlow 尝试将操作放置在设备上时,您会看到前面显示的异常。 如果您更喜欢 TensorFlow 回退到 CPU,则可以将`allow_soft_placement`配置选项设置为`True`
```
with tf.device("/gpu:0"):
i = tf.Variable(3)
i = tf.Variable(3)
config = tf.ConfigProto()
config.allow_soft_placement = True
sess = tf.Session(config=config)
sess.run(i.initializer) # the placer runs and falls back to /cpu:0
```
到目前为止,我们已经讨论了如何在不同设备上放置节点。 现在让我们看看TensorFlow如何并行运行这些节点。
到目前为止,我们已经讨论了如何在不同设备上放置节点。 现在让我们看看 TensorFlow 如何并行运行这些节点。
## 并行运行
TensorFlow运行图时,它首先找出需要评估的节点列表,然后计算每个节点有多少依赖关系。 然后TensorFlow开始评估具有零依赖关系的节点(即源节点)。 如果这些节点被放置在不同的设备上,它们显然会被并行评估。 如果它们放在同一个设备上,它们将在不同的线程中进行评估,因此它们也可以并行运行(在单独的GPU线程或CPU内核中)。
TensorFlow 运行图时,它首先找出需要求值的节点列表,然后计算每个节点有多少依赖关系。 然后 TensorFlow 开始求值具有零依赖关系的节点(即源节点)。 如果这些节点被放置在不同的设备上,它们显然会被并行求值。 如果它们放在同一个设备上,它们将在不同的线程中进行求值,因此它们也可以并行运行(在单独的 GPU 线程或 CPU 内核中)。
TensorFlow管理每个设备上的线程池以并行化操作(参见图12-5)。 这些被称为 inter-op 线程池。 有些操作具有多线程内核:它们可以使用其他线程池(每个设备一个)称为 intra-op 线程池(下面写成内部线程池)。
TensorFlow 管理每个设备上的线程池以并行化操作(参见图 12-5)。 这些被称为 inter-op 线程池。 有些操作具有多线程内核:它们可以使用其他线程池(每个设备一个)称为 intra-op 线程池(下面写成内部线程池)。
![1525242329728](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/5.png?raw=true)
例如,在图12-5中,操作A,B和C是源操作,因此可以立即进行评估。 操作A和B放置在GPU#0上,因此它们被发送到该设备的内部线程池,并立即进行并行评估。 操作A正好有一个多线程内核; 它的计算被分成三部分,这些部分由内部线程池并行执行。 操作C转到GPU#1的内部线程池。
例如,在图 12-5 中,操作`A``B``C`是源操作,因此可以立即进行求值。 操作`A``B`放置在 GPU#0 上,因此它们被发送到该设备的内部线程池,并立即进行并行求值。 操作A正好有一个多线程内核; 它的计算被分成三部分,这些部分由内部线程池并行执行。 操作`C`转到 GPU#1 的内部线程池。
一旦操作C完成,操作D和E的依赖性计数器将递减并且都将达到0,因此这两个操作将被发送到操作内线程池以执行。
一旦操作`C`完成,操作`D``E`的依赖性计数器将递减并且都将达到 0,因此这两个操作将被发送到操作内线程池以执行。
您可以通过设置`inter_op_parallelism_threads`选项来控制内部线程池的线程数。 请注意,您开始的第一个会话将创建内部线程池。 除非您将`use_per_session_threads`选项设置为True,否则所有其他会话都将重用它们。 您可以通过设置`intra_op_parallelism_threads`选项来控制每个内部线程池的线程数。
您可以通过设置`inter_op_parallelism_threads`选项来控制内部线程池的线程数。 请注意,您开始的第一个会话将创建内部线程池。 除非您将`use_per_session_threads`选项设置为`True`,否则所有其他会话都将重用它们。 您可以通过设置`intra_op_parallelism_threads`选项来控制每个内部线程池的线程数。
## 控制依赖关系
在某些情况下,即使所有依赖的操作都已执行,推迟对操作的评估可能也是明智之举。例如,如果它使用大量内存,但在图形中只需要更多内存,则最好在最后一刻对其进行评估,以避免不必要地占用其他操作可能需要的RAM。 另一个例子是依赖位于设备外部的数据的一组操作。 如果它们全部同时运行,它们可能会使设备的通信带宽达到饱和,并最终导致所有等待I / O。 其他需要传递数据的操作也将被阻止。 顺序执行这些通信繁重的操作将是比较好的,这样允许设备并行执行其他操作。
在某些情况下,即使所有依赖的操作都已执行,推迟对操作的求值可能也是明智之举。例如,如果它使用大量内存,但在图形中只需要更多内存,则最好在最后一刻对其进行求值,以避免不必要地占用其他操作可能需要的 RAM。 另一个例子是依赖位于设备外部的数据的一组操作。 如果它们全部同时运行,它们可能会使设备的通信带宽达到饱和,并最终导致所有等待 I/O。 其他需要传递数据的操作也将被阻止。 顺序执行这些通信繁重的操作将是比较好的,这样允许设备并行执行其他操作。
推迟对某些节点的评估,一个简单的解决方案是添加控制依赖关系。 例如,下面的代码告诉TensorFlow仅在评估完a和b之后才评估x和y
推迟对某些节点的求值,一个简单的解决方案是添加控制依赖关系。 例如,下面的代码告诉 TensorFlow 仅在求值完`a``b`之后才求值`x``y`
```
a = tf.constant(1.0)
b = a + 2.0
with tf.control_dependencies([a, b]):
x = tf.constant(3.0)
y = tf.constant(4.0)
x = tf.constant(3.0)
y = tf.constant(4.0)
z = x + y
```
显然,由于z依赖于x和y,所以评估z也意味着等待a和b进行评估,即使它并未显式存在于`control_dependencies()`块中。 此外,由于b依赖于a,所以我们可以通过在[b]而不是[a,b]上创建控制依赖关系来简化前面的代码,但在某些情况下,“显式比隐式更好”。
显然,由于`z`依赖于`x``y`,所以求值`z`也意味着等待`a``b`进行求值,即使它并未显式存在于`control_dependencies()`块中。 此外,由于`b`依赖于`a`,所以我们可以通过在`[b]`而不是`[a,b]`上创建控制依赖关系来简化前面的代码,但在某些情况下,“显式比隐式更好”。
很好!现在你知道了:
......@@ -260,32 +260,32 @@ z = x + y
## 多个服务器的多个设备
要跨多台服务器运行图形,首先需要定义一个集群。 一个集群由一个或多个TensorFlow服务器组成,称为任务,通常分布在多台机器上(见图12-6)。 每项任务都属于一项job(下面称作业)。 作业只是一组通常具有共同作用的任务,例如跟踪模型参数(例如,参数服务器parameter server通常命名为“ps”)或执行计算(这样的作业通常被命名为“ worker”)。
要跨多台服务器运行图形,首先需要定义一个集群。 一个集群由一个或多个 TensorFlow 服务器组成,称为任务,通常分布在多台机器上(见图 12-6)。 每项任务都属于一项作业。 作业只是一组通常具有共同作用的任务,例如跟踪模型参数(例如,参数服务器通常命名为`"ps"`,parameter server)或执行计算(这样的作业通常被命名为`"worker"`)。
![1525243820277](https://github.com/yhcheer/hands_on_Ml_with_Sklearn_and_TF/blob/yh/images/chapter_12/6.png?raw=true)
以下集群规范定义了两个作业“ps”和“worker”,分别包含一个任务和两个任务。 在这个例子中,机器A托管着两个TensorFlow服务器(即任务),监听不同的端口:一个是“ps”作业的一部分,另一个是“worker”作业的一部分。 机器B仅托管一台TensorFlow服务器,这是“worker”作业的一部分。
以下集群规范定义了两个作业`"ps"``"worker"`,分别包含一个任务和两个任务。 在这个例子中,机器A托管着两个 TensorFlow 服务器(即任务),监听不同的端口:一个是`"ps"`作业的一部分,另一个是`"worker"`作业的一部分。 机器B仅托管一台 TensorFlow 服务器,这是`"worker"`作业的一部分。
```
cluster_spec = tf.train.ClusterSpec({
"ps": [
"machine-a.example.com:2221", # /job:ps/task:0
],
"worker": [
"machine-a.example.com:2222", # /job:worker/task:0
"machine-b.example.com:2222", # /job:worker/task:1
]})
"ps": [
"machine-a.example.com:2221", # /job:ps/task:0
],
"worker": [
"machine-a.example.com:2222", # /job:worker/task:0
"machine-b.example.com:2222", # /job:worker/task:1
]})
```
要启动TensorFlow服务器,您必须创建一个服务器对象,并向其传递集群规范(以便它可以与其他服务器通信)以及它自己的作业名称和任务编号。 例如,要启动第一个辅助任务,您需要在机器A上运行以下代码:
要启动 TensorFlow 服务器,您必须创建一个服务器对象,并向其传递集群规范(以便它可以与其他服务器通信)以及它自己的作业名称和任务编号。 例如,要启动第一个辅助任务,您需要在机器 A 上运行以下代码:
```
server = tf.train.Server(cluster_spec, job_name="worker", task_index=0)
```
每台机器只运行一个任务通常比较简单,但前面的例子表明TensorFlow允许您在同一台机器上运行多个任务(如果需要的话)。 如果您在一台机器上安装了多台服务器,则需要确保它们不会全部尝试抓取每个GPU的所有RAM,如前所述。 例如,在图12-6中,“ps”任务没有看到GPU设备,想必其进程是使用`CUDA_VISIBLE_DEVICES =“ ”`启动的。 请注意,CPU由位于同一台计算机上的所有任务共享。
每台机器只运行一个任务通常比较简单,但前面的例子表明 TensorFlow 允许您在同一台机器上运行多个任务(如果需要的话)。 如果您在一台机器上安装了多台服务器,则需要确保它们不会全部尝试抓取每个 GPU 的所有 RAM,如前所述。 例如,在图12-6中,`"ps"`任务没有看到 GPU 设备,想必其进程是使用`CUDA_VISIBLE_DEVICES =""`启动的。 请注意,CPU由位于同一台计算机上的所有任务共享。
如果您希望进程除了运行TensorFlow服务器之外什么都不做,您可以通过告诉它等待服务器使用`join()`方法来完成,从而阻塞主线程(否则服务器将在您的主线程退出)。 由于目前没有办法阻止服务器,这实际上会永远阻止:
如果您希望进程除了运行 TensorFlow 服务器之外什么都不做,您可以通过告诉它等待服务器使用`join()`方法来完成,从而阻塞主线程(否则服务器将在您的主线程退出)。 由于目前没有办法阻止服务器,这实际上会永远阻止:
```
server.join() # blocks until the server stops (i.e., never)
......@@ -301,69 +301,65 @@ b = a + 2
c = a * 3
with tf.Session("grpc://machine-b.example.com:2222") as sess:
print(c.eval()) # 9.0
print(c.eval()) # 9.0
```
这个客户端代码首先创建一个简单的图形,然后在位于机器B(我们称之为主机)上的TensorFlow服务器上打开一个会话,并指示它评估c。 主设备首先将操作放在适当的设备上。 在这个例子中,因为我们没有在任何设备上进行任何操作,所以主设备只将它们全部放在它自己的默认设备上 - 在这种情况下是机器B的GPU设备。 然后它只是按照客户的指示评估c,并返回结果。
这个客户端代码首先创建一个简单的图形,然后在位于机器 B(我们称之为主机)上的 TensorFlow 服务器上打开一个会话,并指示它求值`c`。 主设备首先将操作放在适当的设备上。 在这个例子中,因为我们没有在任何设备上进行任何操作,所以主设备只将它们全部放在它自己的默认设备上 - 在这种情况下是机器 B 的 GPU 设备。 然后它只是按照客户的指示求值`c`,并返回结果。
## 主机和辅助服务
客户端使用gRPC协议(Google Remote Procedure Call)与服务器进行通信。 这是一个高效的开源框架,可以调用远程函数,并通过各种平台和语言获取它们的输出。它基于HTTP2,打开一个连接并在整个会话期间保持打开状态,一旦建立连接就可以进行高效的双向通信。
客户端使用 gRPC 协议(Google Remote Procedure Call)与服务器进行通信。 这是一个高效的开源框架,可以调用远程函数,并通过各种平台和语言获取它们的输出。它基于 HTTP2,打开一个连接并在整个会话期间保持打开状态,一旦建立连接就可以进行高效的双向通信。
数据以协议缓冲区的形式传输,这是另一种开源Google技术。 这是一种轻量级的二进制数据交换格式。
数据以协议缓冲区的形式传输,这是另一种开源 Google 技术。 这是一种轻量级的二进制数据交换格式。
TensorFlow集群中的所有服务器都可能与集群中的任何其他服务器通信,因此请确保在防火墙上打开适当的端口。
TensorFlow 集群中的所有服务器都可能与集群中的任何其他服务器通信,因此请确保在防火墙上打开适当的端口。
每台TensorFlow服务器都提供两种服务:主服务和辅助服务。 主服务允许客户打开会话并使用它们来运行图形。 它协调跨任务的计算,依靠辅助服务实际执行其他任务的计算并获得结果。
每台 TensorFlow 服务器都提供两种服务:主服务和辅助服务。 主服务允许客户打开会话并使用它们来运行图形。 它协调跨任务的计算,依靠辅助服务实际执行其他任务的计算并获得结果。
## 固定任务的操作
通过指定作业名称,任务索引,设备类型和设备索引,可以使用设备块来锁定由任何任务管理的任何设备上的操作。 例如,以下代码将a固定在“ps”作业(即机器A上的CPU)中第一个任务的CPU,并将b固定在“worker”作业的第一个任务管理的第二个GPU (这是A机上的GPU#1)。 最后,c没有固定在任何设备上,所以主设备将它放在它自己的默认设备上(机器B的GPU#0设备)。
通过指定作业名称,任务索引,设备类型和设备索引,可以使用设备块来锁定由任何任务管理的任何设备上的操作。 例如,以下代码将`a`固定在`"ps"`作业(即机器 A 上的 CPU)中第一个任务的 CPU,并将`b`固定在`"worker"`作业的第一个任务管理的第二个 GPU (这是 A 机上的 GPU#1)。 最后,`c`没有固定在任何设备上,所以主设备将它放在它自己的默认设备上(机器 B 的 GPU#0 设备)。
```
with tf.device("/job:ps/task:0/cpu:0")
a = tf.constant(1.0)
a = tf.constant(1.0)
with tf.device("/job:worker/task:0/gpu:1")
b = a + 2
b = a + 2
c = a + b
```
如前所述,如果您省略设备类型和索引,则TensorFlow将默认为该任务的默认设备; 例如,将操作固定到“/ job:ps / task:0”会将其放置在“ps”作业(机器A的CPU)的第一个任务的默认设备上。 如果您还省略了任务索引(例如,“/ job:ps”),则TensorFlow默认为“/ task:0”。 如果省略作业名称和任务索引,则TensorFlow默认为会话的主任务。
如前所述,如果您省略设备类型和索引,则 TensorFlow 将默认为该任务的默认设备; 例如,将操作固定到`"/job:ps/task:0"`会将其放置在`"ps"`作业(机器 A 的 CPU)的第一个任务的默认设备上。 如果您还省略了任务索引(例如,`"/job:ps"`),则 TensorFlow 默认为`"/task:0"`。如果省略作业名称和任务索引,则 TensorFlow 默认为会话的主任务。
## 跨多个参数服务器的分片变量
正如我们很快会看到的那样,在分布式设置上训练神经网络时,常见模式是将模型参数存储在一组参数服务器上(即“ps”作业中的任务),而其他任务则集中在计算上(即 ,“worker”工作中的任务)。 对于具有数百万参数的大型模型,在多个参数服务器上分割这些参数非常有用,可以降低饱和单个参数服务器网卡的风险。 如果您要将每个变量手动固定到不同的参数服务器,那将非常繁琐。 幸运的是,TensorFlow提供了`replica_device_setter()`函数,它以循环方式在所有“ps”任务中分配变量。 例如,以下代码将五个变量引入两个参数服务器:
正如我们很快会看到的那样,在分布式设置上训练神经网络时,常见模式是将模型参数存储在一组参数服务器上(即`"ps"`作业中的任务),而其他任务则集中在计算上(即 ,`"worker"`工作中的任务)。 对于具有数百万参数的大型模型,在多个参数服务器上分割这些参数非常有用,可以降低饱和单个参数服务器网卡的风险。 如果您要将每个变量手动固定到不同的参数服务器,那将非常繁琐。 幸运的是,TensorFlow 提供了`replica_device_setter()`函数,它以循环方式在所有`"ps"`任务中分配变量。 例如,以下代码将五个变量引入两个参数服务器:
```
with tf.device(tf.train.replica_device_setter(ps_tasks=2):
v1 = tf.Variable(1.0) # pinned to /job:ps/task:0
v2 = tf.Variable(2.0) # pinned to /job:ps/task:1
v3 = tf.Variable(3.0) # pinned to /job:ps/task:0
v4 = tf.Variable(4.0) # pinned to /job:ps/task:1
v5 = tf.Variable(5.0) # pinned to /job:ps/task:0
v1 = tf.Variable(1.0) # pinned to /job:ps/task:0
v2 = tf.Variable(2.0) # pinned to /job:ps/task:1
v3 = tf.Variable(3.0) # pinned to /job:ps/task:0
v4 = tf.Variable(4.0) # pinned to /job:ps/task:1
v5 = tf.Variable(5.0) # pinned to /job:ps/task:0
```
您不必传递ps_tasks的数量,您可以传递集群spec `spec = cluster_spec`,TensorFlow将简单计算“ps”作业中的任务数。
如果您在块中创建其他操作,则不仅仅是变量,TensorFlow会自动将它们连接到“/ job:worker”,默认为第一个由“worker”作业中第一个任务管理的设备。 您可以通过设置`worker_device`参数将它们固定到其他设备,但更好的方法是使用嵌入式设备块。 内部设备块可以覆盖在外部块中定义的作业,任务或设备。 例如:
您不必传递`ps_tasks`的数量,您可以传递集群`spec = cluster_spec`,TensorFlow 将简单计算`"ps"`作业中的任务数。
如果您在块中创建其他操作,则不仅仅是变量,TensorFlow 会自动将它们连接到`"/job:worker"`,默认为第一个由`"worker"`作业中第一个任务管理的设备。 您可以通过设置`worker_device`参数将它们固定到其他设备,但更好的方法是使用嵌入式设备块。 内部设备块可以覆盖在外部块中定义的作业,任务或设备。 例如:
```
with tf.device(tf.train.replica_device_setter(ps_tasks=2)):
v1 = tf.Variable(1.0) # pinned to /job:ps/task:0 (+ defaults to /cpu:0)
v2 = tf.Variable(2.0) # pinned to /job:ps/task:1 (+ defaults to /cpu:0)
v3 = tf.Variable(3.0) # pinned to /job:ps/task:0 (+ defaults to /cpu:0)
[...]
s = v1 + v2 # pinned to /job:worker (+ defaults to task:0/gpu:0)
with tf.device("/gpu:1"):
p1 = 2 * s # pinned to /job:worker/gpu:1 (+ defaults to /task:0)
with tf.device("/task:1"):
p2 = 3 * s # pinned to /job:worker/task:1/gpu:1
```
这个例子假设参数服务器是纯CPU的,这通常是这种情况,因为它们只需要存储和传送参数,而不是执行密集计算。
v1 = tf.Variable(1.0) # pinned to /job:ps/task:0 (+ defaults to /cpu:0)
v2 = tf.Variable(2.0) # pinned to /job:ps/task:1 (+ defaults to /cpu:0)
v3 = tf.Variable(3.0) # pinned to /job:ps/task:0 (+ defaults to /cpu:0)
[...]
s = v1 + v2 # pinned to /job:worker (+ defaults to task:0/gpu:0)
with tf.device("/gpu:1"):
p1 = 2 * s # pinned to /job:worker/gpu:1 (+ defaults to /task:0)
with tf.device("/task:1"):
p2 = 3 * s # pinned to /job:worker/task:1/gpu:1
```
这个例子假设参数服务器是纯 CPU 的,这通常是这种情况,因为它们只需要存储和传送参数,而不是执行密集计算。
<!-- translation : alex cheen -->
### Creative RNN
### 生成RNN
到现在为止,我们已经训练了一个能够预测未来时刻样本值的模型,正如前文所述,可以用模型来生成新的序列。
为模型提供 长度为 **n_steps** 的种子序列, 比如全零序列,然后通过模型预测下一时刻的值;把该预测值添加到种子序列的末尾,用最后面 长度为 **n_steps** 的序列做为新的种子序列,做下一次预测,以此类推生成预测序列。
如图14-11所示,这个过程产生的序列会跟原始时间序列相似。
![Figure 14-11](../images/chapter_14/14-11.PNG)
```python
sequence = [0.] * n_steps
for iteration in range(300):
X_batch = np.array(sequence[-n_steps:].reshape(1, n_steps, 1)
y_pred = sess.run(outputs, feed_dict={X: X_batch}
sequence.append(y_pred[0, -1, 0]
```
如果你试图把约翰·列侬的唱片塞给一个RNN模型,看它能不能生成下一张《想象》专辑。
(note: 约翰·列侬 有一张专辑《Imagine》(1971),这里取其双关的意思)
也许你需要一个更强大的RNN网络,它有更多的神经元,层数也更多。下面来探究一下深度RNN。
## 深度RNN
一个朴素的想法就是把一层层神经元堆叠起来,正如图14-12所示的那样,它呈现了一种深度RNN。
![Figure 14-12](../images/chapter_14/14-12.PNG)
为了用TensorFlow实现深度RNN,可先创建一些神经单元,然后堆叠进 **MultiRNNCell**
以下代码中创建了3个相同的神经单元(当然也可以用不同类别的、包含不同不同数量神经元(neurons)的神经单元(cells))
```pyton
n_neurons = 100
n_layers = 3
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([basic_cell] * n_layers)
outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)
```
这些代码就完成了这部分堆叠工作。**status** 变量包含了每层的一个张量(tensor),这个张量就代表了该层神经单元的最终状态(维度为[**batch_size, n_neurons**])。
如果在创建**MultiRNNCell**时 设置了 **state_is_tuple=False**, 那么**status**变量就变成了单个张量,它包含了每一层的状态,其在列的方向上进行了聚合,(维度为[**batch_size, n_layers\*n_neurons**]。
注意在TensorFlow版本0.11.0之前,status是单个张量是默认设置。
## 在多个GPU上分布式部署深度RNN网络
<!-- todo later -->
## Dropout的应用
对于深层深度RNN,在训练集上很容易过拟合。Dropout是防止过拟合的常用技术。
可以简单的在RNN层之前或之后添加一层Dropout层,但如果需要在RNN层之间应用Dropout技术就需要**DropoutWrapper**
下面的代码中,每一层的RNN的输入前都应用了Dropout,Dropout的概率为50%。
```python
keep_prob = 0.5
cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
cell_drop = tf.contrib.rnn.DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([cell_drop]*n_layers)
rnn_outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)
```
同时也可以通过设置**output_keep_prob**来在输出应用Dropout技术。
然而在以上代码中存在的主要问题是,Dropout不管是在训练还是测试时都起作用了,而我们想要的仅仅是在训练时应用Dropout。
很不幸的时DropoutWrapper(还?)不支持 **is_training** 这样一个设置选项。因此必须自己写 Dropout包装类,或者创建两个计算图,一个用来训练,一个用来测试。后则可通过如下面代码这样实现。
```python
import sys
is_training = (sys.argv[-1] == "train")
X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.float32, [None, n_steps, n_outputs])
cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
if is_training:
cell = tf.contrib.rnn.DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([cell]*n_layers)
rnn_outpus, status = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)
[...] # bulid the rest of the graph
init = tf.global_variables_initializer()
saver = tf.train.Saver()
with tf.Session() as sess:
if is_training:
init.run()
for iteration in range(n_iterations):
[...] # train the model
save_path = saver.save(sess, "/tmp/my_model.ckpt")
else:
saver.restore(sess, "/tmp/my_model.ckpt")
[...] # use the model
```
通过以上的方法就能够训练各种RNN网络了。然而对于长序列的RNN训练还言之过早,事情会变得有一些困难。
那么我们来探讨一下究竟这是为什么和怎么应对呢?
## 长时训练的困难
在训练长序列的RNN模型时,那么就需要把RNN在时间维度上展开成很深的神经网络。正如任何深度神经网络一样,其面临着梯度消失/爆炸的问题,使训练无法终止或收敛。
很多之前讨论过的缓解这种问题的技巧都可以应用在深度展开的RNN网络:好的参数初始化方式,非饱和的激活函数(如ReLU),批规范化(Batch Normalization), 梯度截断(Gradient Clipping), 更快的优化器。
即便如此, RNN在处理适中的长序列(如100输入序列)也在训练时表现的很慢。
最简单和常见的方法解决训练时长问题就是在训练阶段仅仅展开限定时间步长的RNN网络,一种称为截断时间反向传播的算法。
在TensorFlow中通过截断输入序列来简单实现这种功能。例如在时间序列预测问题上可以在训练时减小**n_steps**来实现截断。理所当然这种方法会限制模型在长期模式的学习能力。一种变通方案时确保缩短的序列中包含旧数据和新数据,从而使模型获得两者信息(如序列同时包含最近五个月的数据,最近五周的和最近五天的数据)。
问题时如何确保从去年的细分类中获取的数据有效性呢?这期间短暂但重要的事件对后世的影响,甚至时数年后这种影响是否一定要考虑在内呢(如选举结果)?这种方案有其先天的不足之处。
在长的时间训练过程中,第二个要面临的问题时第一个输入的记忆会在长时间运行的RNN网络中逐渐淡去。确实,通过变换的方式,数据穿流在RNN网络之中,每个时间步长后都有一些信息被抛弃掉了。那么在一定时间后,第一个输入实际上会在RNN的状态中消失于无形。
比如说,你想要分析长篇幅的影评的情感类别,影评以“I love this moive”开篇,并辅以各种改善影片的一些建议。试想一下,如果RNN网络逐渐忘记了开头的几个词,RNN网络的判断完全有可能会对影评断章取义。
为了解决其中的问题,各种能够携带长时记忆的神经单元的变体被提出。这些变体是有效的,往往基本形式的神经单元就不怎么被使用了。
首先了解一下最流行的一种长时记忆神经单元:长短时记忆神经单元LSTM。
## LSTM 单元
长短时记忆单元在1997年由S.H. 和 J.S.首次提出(https://goo.gl/j39AGv) [^3],并在接下来的几年内经过A.G,H.S(https://goo.gl/6BHh81)[^4],W.Z(https://goo.gl/SZ9kzB)[^5] 等数位研究人员的改进逐渐形成。如果把LSTM单元看作一个黑盒,从外围看它和基本形式的记忆单元很相似,但LSTM单元会比基本单元性能更好,收敛更快,能够感知数据的长时依赖。TensorFlow中通过**BasicLSTMCell**实现LSTM单元。
[^3]: "Long Short-Term Memory," S.Hochreiter and J.Schmidhuber(1997)
[^4]: "Long Short-Term Memory Recurrent Neural Network Architectures for Large Scale Acoustic Modeling," H.Sak et al.(2014)
[^5]: "Recurrent Neural Network Regularization," W.Zaremba et al.(2015)
```python
lstm_cell = tf.contrib.rnn.BasicLSTMCell(num_units=n_neurons)
```
LSTM单元的工作机制是什么呢?在图14-13中展示了基本LSTM单元的结构。
![Figure 14-13](../images/chapter_14/14-13.PNG)
不观察LSTM单元内部,除了一些不同外跟常规RNN单元极其相似。这些不同包括LSTM单元状态分为两个矢量:**h**<sub>(t)</sub>**c**<sub>(t)</sub>(c代表cell)。可以简单认为**h**<sub>(t)</sub>是短期记忆状态,**c**<sub>(t)</sub>是长期记忆状态。
好,我们来打开盒子。LSTM单元的核心思想是其能够学习从长期状态中存储什么,忘记什么,读取什么。长期状态**c**<sub>(t-1)</sub>从左向右在网络中传播,依次经过遗忘门(**forget gate**)时丢弃一些记忆,之后加操作增加一些记忆(从输入门中选择一些记忆)。输出**c**<sub>(t)</sub>不经任何转换直接输出。每个单位时间步长后,都有一些记忆被抛弃,新的记忆被添加进来。另一方面,长时状态经过tanh激活函数通过输出门得到短时记忆**h**<sub>(t)</sub>,同时它也是这一时刻的单元输出结果**y**<sub>(t)</sub>。接下来讨论一下新的记忆时如何产生的,门的功能时如何实现的。
首先,当前的输入矢量**x**<sub>(t)</sub>和前一时刻的短时状态**h**<sub>(t-1)</sub>作为输入传给四个全连接层,这四个全连接层有不同的目的:
- 其中主要的全连接层输出**g**<sub>(t)</sub>,它的常规任务就是解析当前的输入**x**<sub>(t)</sub>和前一时刻的短时状态**h**<sub>(t-1)</sub>。在基本形式的RNN单元中,就与这种形式一样,直接输出了**h**<sub>(t)</sub>**y**<sub>(t)</sub>。与之不同的时LSTM单元会将一部分**g**<sub>(t)</sub>存储在长时状态中。
- 其它三个全连接层被称为门控制**gate controllers**。其采用Logistic作为激活函数,输出范围在0到1之间。正如在结构图中所示,这三个层的输出提供给了元素积的乘操作,当输入为0时门关闭,输出为1时门打开。分别地:
- 忘记门(**forget gate**)由**f**<sub>(t)</sub>控制,来决定哪些长期记忆需要被擦出;
- 输入门(**input gate**) 由**i**<sub>(t)</sub>控制,它的作用是处理哪部分**g**<sub>(t)</sub>应该被添加到长时状态中,也就是为什么被称为**部分存储**
- 输出门(**output gate**)由**o**<sub>(t)</sub>控制,在这一时刻的输出**h**<sub>(t)</sub>**y**<sub>(t)</sub>就是由输出门控制从长时状态中读取的记忆。
简要来说,LSTM单元能够学习到识别重要输入(输入门作用),存储进长时状态,并保存必要的时间(忘记门功能),并学会提取当前输出所需要的记忆。
这也解释了LSTM单元能够在提取长时序列,长文本,录音等数据中的长期模式的惊人成功的原因。
方程式14-3总结了如何计算单元的长时状态,短时状态,和单个输入情形时每单位步长的输出(小批量的方程形式与单输入的形式相似)。
![Equation 14-3](../images/chapter_14/14-3E.PNG)
- **W**<sub>xi</sub>,**W**<sub>xf</sub>,**W**<sub>xo</sub>,**W**<sub>xg</sub>是四个全连接层关于输入矢量**x**<sub>(t)</sub>的权重。
- **W**<sub>hi</sub>,**W**<sub>hf</sub>,**W**<sub>ho</sub>,**W**<sub>hg</sub>是四个全连接层关于上一时刻的短时状态**h**<sub>(t-1)</sub>的权重。
- **b**<sub>i</sub>,**b**<sub>f</sub>,**b**<sub>o</sub>,**b**<sub>g</sub>,时全连接层的四个偏执项,需要注意的时TensorFlow初始化该项为全1矢量,而非全0,时阻止网络初始训练状态下各个门关闭从而忘记所有记忆。
### 窥孔连接
基本形式的LSTM单元中,门的控制仅有当前的输入**x**<sub>(t)</sub>和前一时刻的短时状态**h**<sub>(t-1)</sub>。不妨让各个控制门窥视一下长时状态,获取一些上下文信息不失为一种尝试。该想法(https://goo.gl/ch8xz3)由F.G.he J.S.在2000年提出。他们提出的LSTM的变体有叫做窥孔连接的额外连接:把前一时刻的长时状态**c**<sub>(t-1)</sub>加入忘记门和输入门的控制输入,当前时刻的长时状态加入输出门的控制输入。
TensorFLow中由LSTMCell实现以上变体LSTM,并设置use_peepholes=True.
```python
lstm_cell = tf.contrib.rnn.LSTMCell(num_units=n_neurons, use_peepholes=True)
```
在众多LSTM变体中,一个特别流行的变体就是GRU单元。
## GRU单元
![Figure 14-14](../images/chapter_14/14-14.PNG)
门控循环单元(图 14-14)在2014年K.Cho et al.的论文(http://goo.gl/ZnAEOZ) 中提出,并在此文中也引入了前文所述的编码-解码网络。
门控循环单元时LSTM单元的简化版本,其能实现同样的性能,这也说明了为什么它能越来越流行。简化主要在一下几个方面:
- 长时状态和短时状态合并为一个矢量**h**<sub>(t)</sub>
- 用同一个门控制忘记门和输入门。如果门控制输入1,输入门打开,忘记门关闭,反之亦然。也就是说,如果当有新的记忆需要存储,那么就必须实现在其对应位置事先擦除该处记忆。这也构成了LSTM本身的常见变体。
- GRU单元取消了输出门,单元的全部状态就是该时刻的单元输出。与此同时,增加了一个控制门**r**<sub>(t)</sub>来控制哪部分前一时间步的状态在该时刻的单元内呈现。
![Equation 14-4](../images/chapter_14/14-4E.PNG)
方程式14-4总结了如何计算单个输入情形时每单位步的单元的状态。
在TensoFlow中创建GRU单元很简单:
```python
gru_cell = tf.contrib.rnn.GRUCell(n_units=n_neurons)
```
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册