Tensorpack架构
DataFlow
是一个用于在Python中高效地加载数据的库。除了DataFlow之外,本地TF运营商也可以用于数据加载。它们最终将被封装在相同的InputSource
接口下并进行预取。- 可以使用任何基于TF的符号函数库来定义模型,其中包括tensorpack中的一小组函数。
ModelDesc
是连接模型的接口和InputSource
的接口。 - Tensorpack的
Trainers
用于管理训练过程中的循环迭代。它们还包括用于多GPU或分布式训练的数据并行逻辑。同时,也拥有很强大的定制能力。 Callbacks
就像tf.train.SessionRunHook
或者plugins
。在训练过程中,除了主迭代以外,您想要做的所有事情都可以通过回调进行定义并轻松重用。- 所有组件尽管完美地结合在一起,但都具有高度的去相关性:您可以:
- 单独使用DataFlow作为数据加载库,根本不用tensorfow。
- 使用tensorpack构建具有多GPU或分布式支持的图结构,然后使用自己的循环进行训练。
- 自行构建图形,并使用tensorpack回调进行训练。
DataFlow(数据流接口)
DataFlow是什么?
DataFlow是一个用于构建Python迭代器以提高数据加载效率的库。
定义:DataFlow有一个get_data()
生成器方法,它产生数据点(datapoints
)。datapoints是被称为数据点组件(components of a datapoin)的Python列表对象。
例子:要训练MNIST数据集,需要使用DataFlow
的get_data()
方法,该方法需要生成两个组件的数据点(列表):一个形状为(64,28,28)
的numpy的数组(图片数据)和一个形状(64, )
数组(图片标签)。
DataFlow的组成
有一个标准接口的好处是能够提供最大的代码可重用性。 tensorpack中有很多现有的DataFlow实用程序,您可以使用这些实用程序来组合具有长数据管道的复杂DataFlow。常见的流水线通常会从磁盘(或其他来源)读取,应用转换( apply transformations),分组批处理(group into batches) ,预取数据(group into batches)。一个简单的例子如下:
1 | # a DataFlow you implement to produce [tensor1, tensor2, ..] lists from whatever sources: |
为什么要使用DataFlow
- 很简单:用纯Python编写所有内容,并重用现有的实用程序。相反,在TF操作员中编写数据加载器通常很痛苦,性能很难调整。
- 速度非常快:可以构建具有并行性的快速DataFlow。在tensorpack中使用DataFlow,可以采用
Input Pipeline
,进一步加速图形中的数据加载。
尽管如此,tensorpack还支持用本地TF操作与TF数据集加载数据。
使用DataFlow(Tensorpack外部)
通常,tensorpack InputSource
接口将DataFlow链接到图结构进行训练。如果在某些自定义代码中使用DataFlow,需要首先调用reset_state()
初始化,然后使用生成器:
1 |
|
DataFlow独立于tensorpack和TensorFlow。要导入tensorpack.dataflow,甚至不必安装TensorFlow。可以简单地将其用作数据处理管道并将其插入任何其他框架。
Input Pipeline(输入流水线)
本教程包含关于“如何高效地读取数据以使用TensorFlow”以及tensorpack如何支持这些方法的主题的一般性讨论。 您不必阅读它,因为这些是tensorpack接口下的细节,但知道它可以帮助您在构建任务时,高效的选择最佳输入管道。
数据准备并行化
无论使用什么框架,都可以有一个常识:Prepare data in parallel with the training!
原因如下:
- 数据准备通常会耗费不重要的时间(取决于实际问题)。
- 数据准备与训练是独立的!这个并行的最根本前提!
- 数据准备与训练通常使用完全不同的资源(参见上图):Training过程使用GPU,加载数据的过程通过磁盘,处理数据采用CPU,拷贝数据到TF经过的是内存带宽,而拷贝到GPU是通过PCI-e总线。因此训练和数据准备两项任务一起完成并不会降低工作效率。事实上,你可以进一步并行化数据准备实现的不同阶段,因为他们也使用不同的资源。
我们可以简单的计算一下:
4台P100 GPU可以852张/秒的速度训练ResNet50,这些图像的大小为852 224 224 3 4bytes = 489MB。 假设你有 5GB / s的memcpy带宽(如果你运行单线程拷贝,大致就像这样),只需复制一次数据就需要0.1s ——将训练速度减慢10%。 并且在预处理期间还有很多运算成本依然需要耗费大量时间!。
未能隐藏数据准备延迟是看不到良好GPU利用率的主要原因。 因此一定要选择一个能够延迟隐藏的框架。 但是大多数其他TensorFlow封装都是基于feed_dict
设计的。 Tensorpack有内置的机制来隐藏上述阶段的延迟。 这是Tensorpack速度更快的主要原因。
Python Reader or TF Reader ?
无论您使用什么来加载/预处理数据,上述讨论都是有效的,无论是Python代码还是TensorFlow运算符,或者是两者的组合。这两个都支持tensorpack,推荐使用Python。
TensorFlow Reader优点
人们经常认为他们应该使用tf.data,因为它很快。
- 事实上,它一般情况下很快,但不一定。使用Python,您可以访问许多其他快速库,这些库在TF中可能不受支持。
- Python足够快。只要数据准备与训练保持同步,并且上图中所有四个模块的延迟都隐藏起来,更快的读取不会对整体吞吐量产生任何影响。 对于大多数类型的问题,直到多GPU ImageNet训练的规模,如果您使用快速库(例如
tensorpack.dataflow
),Python可以提供足够的速度。
TensorFlow Reader缺点
TensorFlow Reader的缺点显而易见——接口设计太复杂了!
与运行数学模型不同,数据处理是一项复杂且结构不良( poorly-structured)的任务。 您需要处理不同的格式,处理特殊案例,嘈杂的数据,数据组合。 这样做需要条件操作,循环,数据结构,有时甚至是异常处理。 这些操作自然不是符号图的该做的相关任务。
我们来看看用户对tf.data的要求:
- 填充数据,混洗数据的不同方式
- 处理数据中的任何值
- 处理不是批量大小倍数的数据集
- 排序/跳过一些数据
- 将数据写入文件
为了支持所有这些可以在Python中使用3行代码完成的功能,您需要一个新的TF API,或者向Dataset.from_generator(即Python再次)提出要求。
如果您的数据本来就非常干净并且格式正确,那么使用TF来读取数据才有意义。如果没有,你可能会想写一个脚本来格式化你的数据,但是你几乎已经写了一个Python加载器了!
考虑一下:编写一个Python脚本以便从某种格式转换为TF友好格式,然后是从这种格式转换为张量的TF脚本是浪费时间。 中间格式不一定存在。 您只需要正确的界面即可直接高效地将Python连接到图形。 tensorpack.InputSource
就是这样的一个接口。
InputSource
InputSource
是tensorpack中的抽象接口,用于描述输入来自哪里以及它们如何进入图形。例如,
- FeedInput:来自DataFlow并获取图表(缓慢)。
- QueueInput:来自DataFlow并由TF队列缓存在CPU上。
- StagingInput:来自某个
InputSource
,然后由TF StagingArea在GPU上预取。 - TFDatasetInput:来自
tf.data.Dataset
。 - dataflow_to_dataset:来自DataFlow,并由
tf.data.Dataset
进一步处理。 - TensorInput:来自你定义的张量(例如可以是读操作)。
- ZMQInput:来自一些ZeroMQ管道,读取/预处理可能发生在不同的过程中,甚至不同的机器中。
通常,我们推荐
QueueInput + StagingInput
,因为它对大多数用例都很有用。 如果您的数据因任何原因必须来自单独的进程,请使用ZMQInput
。 如果您仍然想使用TF读取操作,请定义一个tf.data.Dataset
并使用TFDatasetInput
。
Symbolic Layers(符号层)
Tensorpack包含一小部分通用模型基元,如conv / deconv,fc,bn,pooling层。 这些层的编写只是因为在tensorpack开发时没有其他选择。 现在,这些实现实际上可以直接调用tf.layers
。
现在,可以在tensorpack中使用tf.layers或任何其他符号库。使用tensorpack实现,您还可以通过argscope
和LinearWrap
,以简化代码。
请注意,为了保持代码和预先训练模型的向后兼容性,tensorpack层与tf.layers有一些细微差别,包括变量名称和默认选项。有关详细信息,请参阅tensorpack API文档。
argscope and LinearWrap
argscope
为您提供默认参数的上下文。 LinearWrap
是简化构建“线性结构”模型的糖衣语法(syntax sugar)。
代码如下:1
2
3
4
5
6
7
8
9
10
11with argscope(Conv2D, filters=32, kernel_size=3, activation=tf.nn.relu):
l = (LinearWrap(image) # the starting brace is only for line-breaking
.Conv2D('conv0')
.MaxPooling('pool0', 2)
.Conv2D('conv1', padding='SAME')
.Conv2D('conv2', kernel_size=5)
.FullyConnected('fc0', 512, activation=tf.nn.relu)
.Dropout('dropout', rate=0.5)
.tf.multiply(0.5)
.apply(func, *args, **kwargs)
.FullyConnected('fc1', units=10, activation=tf.identity)())
上面示例代码等价如下:
1 | l = Conv2D('conv0', image, 32, 3, activation=tf.nn.relu) |
访问相关的张量
层中的变量将命名为name / W
,name / b
等。有关详细信息,请参阅每个图层的API文档。在构建图时,可以像这样访问变量:
1 | l = Conv2D('conv1', l, 32, 3) |
但请注意,这是一种很冒险的方式,可能不适用于未来版本的TensorFlow。此方法也不适用于LinearWrap,并且无法访问由激活函数创建的变量。除非在API中有不同的说明,否则层的输出通常被命名为
name/output
。你可以打印张量来查看它的名字。
在Tensorpack外使用模型
您可以单独使用tensorpack模型作为简单的符号函数库。为此,只需在定义模型时输入TowerContext
:
1 | with TowerContext('', is_training=True): |
某些层(特别是BatchNorm)具有不同的训练/测试时间行为,此行为由TowerContext控制。如果您需要在测试时间使用它们的tensorpack版本,则需要在另一个上下文中为它们创建操作。
1 | # Open a `reuse=True` variable scope here if you're sharing variables, then: |
在Tensorpack中使用其他符号库
在定义模型的过程中可以使用任何觉得舒服的库来构建图结构。
通常,slim
/ tflearn
/ tensorlayer
只是符号函数包装器,调用它们与调用tf.add
没什么区别。 不过,需要小心如何正规化/ BN更新应该在这些库中的处理。
这与使用 sonnet
/ Keras
有点不同。 sonnet
/ Keras
通过自己的模型类来管理变量范围,调用它们的符号函数总是创建新的变量范围。 请参阅Keras示例以了解如何在tensorpack内使用它。
Trainers
Tensorpack训练器包含以下逻辑:
- 构件图结构
- 运行迭代(带回调)
通常我们不会直接触摸这些方法,而是在训练器上使用更高级的接口。我们只需选择要使用的训练器。但是他们如何工作的一些基本知识是有用的:
Tower Trainer
遵循TensorFlow中的术语,Tower Trainer
是一个可调用的函数,它接受输入张量并将模型的一个重复项添加到图中。这种概念可以描述大多数类型的神经网络训练。Tower
的概念主要用于支持:
- 数据并行多GPU训练,其中在每个GPU上构建复制品。
- 用于推理的图构造,其中复制是在推理模式下构建的。
用户需要提供Tower函数才能使用TowerTrainer
。特别是,在使用ModelDesc
接口时,build_graph
方法将成为Tower函数。
Tower函数需要遵循一些约定:
- 它可能被多次调用以进行数据并行训练或推理。
- 因此,要使用tensorflow-hub模块,需要在tower函数外初始化模块,并在tower函数内调用模块。
- 它必须遵循变量集合
- (必需)只能将可通过梯度下降调整的变量放入
TRAINABLE_VARIABLES
中。 - (推荐)将需要用于推理的不可训练变量放入
MODEL_VARIABLES
中。
- (必需)只能将可通过梯度下降调整的变量放入
- 它必须遵循变量范围:
- 在函数中创建的任何可训练变量的名称必须与“variable_scope_name / custom / name”类似。不要依赖于name_scope的名字。不要使用两次variable_scope的名字。
- 创建任何可训练变量都必须遵守重用变量范围。要遵守变量重用,请在函数中使用
tf.get_variable
而不是tf.Variable
。另一方面,对于不可训练的变量,可以使用tf.Variable
确保在每个tower中创建新变量,即使reuse= True
时也是如此。 - 总是在
TowerContext
下调用,可以通过get_current_tower_contxt()
来访问它。上下文包含有关训练/推理模式,重用等信息。这些约定很容易遵循,并且大多数层封装(例如,tf.layers / slim / tensorlayer)确实遵循它们。 请注意,某些Keras层不遵循这些约定,如果在tensorpack中使用,将需要一些变通方法。
当然你也可以不这样编写,但所有现有的tensorpack trainers都是TowerTrainer的子类。
MultiGPU Trainers
对于数据并行多GPU训练,不同的多GPU训练将实施不同的分配策略。他们以有效的方式照顾器件布局,梯度平均和同步,并且都达到了官方TF基准测试的相同性能。只需要一行代码更改即可使用它们,即trainer=SyncMultiGPUTrainerReplicated()
。
请注意使用这些trains时的一些常见问题:
在每次迭代中,所有GPU(模型的所有复制品)从InputSource中获取张量,而不是全部和分割。 所以总的批量大小将变成
(batch size of InputSource) * #GPU
为数据并行训练分割张量根本没有意义。 首先,为什么要将时间串联起来分成大批量然后再分开呢? 其次,这会对数据造成不必要的形状限制。 通过让每个GPU训练自己的输入张量,他们可以同时训练不同形状的输入。
tower函数(您的模型代码)将被称为乘法时间。 因此,在修改这些函数中的全局状态时需要小心,例如 将操作添加到TF集合中。
Distributed Trainers
分布式培训需要提供高性能allreduce实现的horovod库。要运行分布式培训,首先正确安装horovod,然后参阅HorovodTrainer的文档。
Tensorpack已经使用TF的本地API实现了一些其他分布式训练,但即使在今天,TF对分布式训练的本地支持也不是很高的性能。 因此这些训练没有积极维护,不推荐使用。
Training Interface
Tensorpack有一个详尽的接口以获得最大的灵活性。当不想过多定制,训练时可以使用tensorpack提供的接口来简化代码。
使用ModelDesc和TrainConfig
这是旧Tensorpack用户最熟悉的接口,仅用于单一成本任务( single-cost tasks )。这个接口有很多例子。SingleCost trainers
需要4个参数来设置图:InputDesc
,InputSource
,get_cost
函数和optimizer
。ModelDesc
通过将它们中的三个打包成一个对象来描述一个模型:
1 | class MyModel(ModelDesc): |
_get_inputs
需要定义构建图的所有输入的元信息。
_build_graph
获取与_get_inputs
匹配的inputs
张量列表。
在_build_graph
中可以使用任何符号函数,包括TensorFlow核心库函数和其他符号库。_build_graph
是tower函数,所以需要遵循一些规则。同时还需要在此函数中设置self.cost
。
定义了这样一个模型后,使用TrainConfig
和launch_train_with_config
:
1 | config = TrainConfig( |
Raw Trainer Interface
要获得较低级别的控件,您还可以直接访问训练的方法:
- 构建图:对于一般训练,请自行构建图结构。 对于单成本训练( single-cost trainer),请通过
SingleCostTrainer.setup_graph
构建图结构。 - 运行迭代:调用
Trainer.train()
或`Trainer.train_with_defaults()``,它为正常用例提供了一些默认选项。
Callbacks
回调是除了训练迭代以外的其他所有事情的接口。
除了最小化cost的实际训练迭代之外,你可能想要做一些其他事情。如下:
- 在训练开始之前(例如:初始化保存程序,转储图)
- 随着每次训练迭代(例如,在图中运行一些其他操作)
- 在训练迭代之间(例如更新进度条,更新超参数)
- 在周期之间(例如保存模型,运行一些验证)
- 训练后(例如,在某处发送模型,发送消息到您的手机)
人们传统上倾向于将训练循环与这些额外功能一起编写。这会使循环冗长,同一特征的代码可能会分开(设想一个特征,它需要在开始时进行初始化,然后在迭代之间进行一些实际工作)。
通过编写回调来实现在每个地方做什么,tensorpack训练器将在适当的时候调用回调。因此,只要您使用tensorpack,这些功能就可以一条线(single line)的重复使用。
例如,这些在训练ResNet时使用的回调:
1 |
|
请注意,回调涵盖了训练的每个细节,从图操作到进度条。这意味着我们可以根据自己的喜好自定义训练的各个部分,例如,在进度条上显示不同的内容,以不同训练阶段评估部分结果等。
这些功能并不总是必要的,但是想想如果你将这些逻辑与循环一起写入,主循环看起来有多混乱。如果您在需要时只用一条线即可启用这些功能,那么将变得多么简单。
Save and Load models
Work with TF Checkpoint
ModelSaver
回调以TensorFlow检查点(checkpoint)格式将模型保存到logger.get_logger_dir()
定义的目录中。TF检查点通常包含.data-xxxxx
文件和.index
文件。两者都是必要的。tf.train.NewCheckpointReader
是解析TensorFlow检查点的最佳工具。我们有两个示例脚本来演示它的用法,但请阅读TF文档以获取详细信息。
scripts/ls-checkpoint.py
演示如何在检查点打印所有变量及其形状。scripts/dump-model-params.py
可用于删除检查点中不必要的变量。它需要一个metagraph
文件(它也保存在ModelSaver中),只保存模型在推理时需要的变量。它可以将模型转储到以npz格式保存的var-name:value
字典中。
Load a Model
模型加载(在训练或测试中)是通过session_init
接口。 目前有两种方法可以恢复会话:session_init = SaverRestore(…)
来恢复TF检查点,或session_init = DictRestore(…)
来恢复字典。get_model_loader
帮助决定使用文件名中哪一个。要加载多个模型,请使用ChainInit
。
变量还原完全基于当前图中变量与session_init初始值设定项中的变量之间的名称匹配。仅出现在一侧的变量将被打印为警告。
Transfer Learning
迁移学习是很简单的。如果你想加载一些模型,只需使用相同的变量名称即可。如果你想重新训练一些层,只需重新命名相关层即可。
Summary and Logging
在训练期间,除迭代以外的所有内容均通过回调执行。本教程将解释如何在回调中处理汇总和日志记录,以及如何定制它们。默认的日志记录行为应该足够用于正常的用例,所以你可以跳过本教程。
TensorFlow Summaries
这是TensorFlow如何最终被记录/保存/打印的总结:
- What to Log:当在构造图代码中调用
tf.summary.xxx
时,TensorFlow会将一个操作添加到tf.GraphKeys.SUMMARIES
集合(默认情况下)。 - When to Log:
MergeAllSummaries
回调位于默认回调中。它每个周期(默认情况下)在SUMMARIES
集合(默认情况下)运行,并将结果写入监视器。 - Where to Log:默认情况下启用多个监视器。
TFEventWriter
通过tensorboard
将东西写入所使用的事件中。ScalarPrinter
打印终端中的所有标量。JSONWriter
将标量保存到JSON文件。
所有的 “what, when, where”可以在图或者回调(callbacks)/监视器(monitors)设置中自定义。
由于TF summaries
在默认情况下不会频繁被评估(每个epoch),如果内容是依赖于数据的,那么这些值可能具有很高的方差。 为了解决这个问题,你可以:
- 更改“What to Log”:更频繁地记录日志,但请注意,记录某些summaries可能很昂贵。可能需要使用单独的集合进行频繁日志记录。
- 更改“When to Log”:可以在标量张量上调用
tfutils.summary.add_moving_summary
,它将总结这些标量的移动平均值,而不是它们的即时值。移动平均由MovingAverageSummary
回调(默认启用)维护。
Other Logging Data
除了TensorFlow summaries外,回调还可以在训练开始后的任何时候通过self.trainer.monitors.put_xxx
将其他数据写入监视器后端。只要支持数据类型,数据将被分派到并记录到同一个地方。
因此,tensorboard不仅会显示图中的信息,还会显示我们自定义的数据。例如,验证集的准确度通常需要在TensorFlow图之外计算。通过一个统一的监视器后端,这个数字也会显示在tensorboard上。
Inference
Inference During Training
训练期间有两种方法可以进行推理。
- 最简单的方法是编写回调函数,并使用
self.trainer.get_predictor()
在推理模式下获取可调用的函数。 - 如果推理过程中遵循以下范例:“每个输入获取一些张量,并汇总结果”。可以使用
InferenceRunner
接口和一些Inferencer
。这将进一步支持预取和数据并行推断。
在这两种方法中,tower函数都会被再次调用,使用TowerContext.is_training == False
构建不同的图。
Inference After Training
Tensorpack不关心训练后的操作。它将模型保存为标准检查点格式,以及metagraph protobuf文件。它们足以用于TensorFlow进行任何部署方法。但是您需要阅读TF文档并自行完成。