在 TF2 中,Keras 利用 tf.function
为模型构建其训练、评估和预测的图形。我们将它们称为“执行函数”。在 TF1 中,“执行函数”是 FuncGraph,它与 TF 函数共享一些公共组件,但具有不同的实现。
在过程中,我们不知怎么地留下了 train_on_batch()、test_on_batch() 和 predict_on_batch() 的不正确实现。它们在数值上仍然是正确的,但 x_on_batch 的执行函数是一个纯 Python 函数,而不是一个被 tf.function 包装的 Python 函数。这将导致速度变慢。
在 TF2 中,我们将所有输入数据转换为 tf.data.Dataset,通过这种方式,我们可以统一执行函数以处理单一类型的输入。可能会有一些开销在数据集转换时出现,我认为这是一次性的开销,而不是每个批次的成本。
针对 Eager 模式的速度问题,我们使用了 @tf.function,它会将一个 Python 函数转换为图形。当提供数值值(例如 np 数组)时,tf.function 的主体被转换为静态图形,并进行优化,返回最终值,这是快速且应该具有类似 TF1 图形模式的性能。
我不同意上段的最后一句话,并根据我的分析结果指出,Eager 的输入数据处理速度比 Graph 快得多。此外,我不确定特别是 tf.data.Dataset,但 Eager 反复调用多个相同的数据转换方法 - 请参见分析器。
最后,开发人员关联的提交:大量更改以支持 Keras v2 循环。
Train Loops: 根据(1)Eager vs. Graph;(2)输入数据格式,训练将使用不同的 train loop 进行 - 在 TF2 中,_select_training_loop()
,training.py,其中之一:training_v2.Loop()
training_distributed.DistributionMultiWorkerTrainingLoop(
training_v2.Loop())
training_distributed.DistributionMultiWorkerTrainingLoop(
training_distributed.DistributionSingleWorkerTrainingLoop())
training_generator.GeneratorOrSequenceTrainingLoop()
training_generator.EagerDatasetOrIteratorTrainingLoop()
training_generator.GeneratorLikeTrainingLoop()
training_arrays.ArrayLikeTrainingLoop()
每个处理资源分配方式不同,对性能和功能有不同的影响。
训练循环:fit vs train_on_batch,keras vs tf.keras:这四个中的每一个使用不同的训练循环,尽管可能没有每种可能的组合都有。例如,keras
的fit
使用一种fit_loop
形式,例如training_arrays.fit_loop()
,其train_on_batch
可能使用K.function()
。 tf.keras
具有更复杂的层次结构,在前面的部分中部分描述。
训练循环:文档 - 有关某些不同执行方法的相关源docstring:
与其他TensorFlow操作不同,我们不会将python数值输入转换为张量。此外,为每个不同的python数值生成一个新图形
function
为每组唯一的输入形状和数据类型实例化单独的图形。
单个 tf.function
对象可能需要映射到多个计算图形,这应该仅作为性能可见(跟踪图形具有非零的计算和内存成本)
输入数据处理器:与上面类似,根据运行时配置(执行模式,数据格式,分发策略)设置内部标志来逐个选择处理器。最简单的情况是使用Eager,它直接使用Numpy数组。有关一些特定示例,请参见此答案。
模型大小,数据大小:
- 是决定性的;没有一个单一的配置位于所有模型和数据大小之上。
- 相对于模型大小的数据大小很重要;对于小数据和模型,数据传输(例如CPU到GPU)开销可能占主导地位。同样,小的开销处理器在大的数据上运行可能会更慢,每个数据转换时间支配(请参见“ PROFILER”中的
convert_to_tensor
) - 由于训练循环和输入数据处理器处理资源的方式不同,因此速度也有所不同。
基准测试:磨碎的肉。 - Word文档 - Excel电子表格
术语:
- %-less数字均为秒数
- %计算公式为
(1-longer_time/shorter_time)*100
;原理:我们感兴趣的是一个比另一个快了多少倍;shorter / longer
实际上是一种非线性关系,不适用于直接比较 - %符号的确定:
- TF2 vs TF1:
+
如果TF2更快 - GvE(图形vs. Eager):
+
如果Graph更快
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1;TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PROFILER:
PROFILER - 说明: Spyder 3.3.6 IDE profiler.
某些函数在其他函数的嵌套中重复出现;因此,很难跟踪“数据处理”和“训练”函数之间的确切分离,因此会有一些重叠 - 正如最后一个结果所表明的那样。
%数字是相对于减去构建时间的运行时间计算的。
构建时间通过将所有(唯一)调用了1或2次的运行时间相加来计算
训练时间通过将所有(唯一)调用次数与迭代次数相同的运行时间以及它们的一些嵌套运行时间相加来计算
函数根据其原始名称进行剖析,不幸的是(即_func = func
将作为func
进行剖析),这混合了构建时间-因此需要排除它
测试环境:
- 以最少的后台任务运行代码
- 按照这篇文章中建议,进行了几次迭代以预热GPU
- CUDA 10.0.130、cuDNN 7.6.0、TensorFlow 1.14.0 和 TensorFlow 2.0.0都是通过源代码编译构建的,还使用了Anaconda
- Python版本为3.7.4,使用Spyder 3.3.6 IDE
- GTX 1070显卡,安装的操作系统是Windows 10,具有24GB DDR4 2.4-MHz RAM和i7-7700HQ 2.8-GHz CPU
方法:
- 基准测试'小型'、'中型'和'大型'模型数据集
- 为每个模型大小固定参数数量,与输入数据大小无关
- '更大'的模型具有更多的参数和层数
- '更大'的数据具有更长的序列,但相同
batch_size
和num_channels
- 模型仅使用
Conv1D
、Dense
'可学习'层;避免使用RNN以防止TF版本实现差异
- 总是在基准测试循环之外运行一个train fit,以省略模型和优化器图形构建
- 不使用稀疏数据(例如
layers.Embedding()
)或稀疏目标(例如SparseCategoricalCrossEntropy()
限制: "完整"的答案将解释每个可能的训练循环和迭代器,但这显然超出了我的时间能力、不存在的工资或一般必要性。结果仅与方法一样好 - 开放心态地解释。
代码:
import numpy as np
import tensorflow as tf
import random
from termcolor import cprint
from time import time
from tensorflow.keras.layers import Input, Dense, Conv1D
from tensorflow.keras.layers import Dropout, GlobalAveragePooling1D
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
def reset_seeds(reset_graph_with_backend=None, verbose=1):
if reset_graph_with_backend is not None:
K = reset_graph_with_backend
K.clear_session()
tf.compat.v1.reset_default_graph()
if verbose:
print("KERAS AND TENSORFLOW GRAPHS RESET")
np.random.seed(1)
random.seed(2)
if tf.__version__[0] == '2':
tf.random.set_seed(3)
else:
tf.set_random_seed(3)
if verbose:
print("RANDOM SEEDS RESET")
print("TF version: {}".format(tf.__version__))
reset_seeds()
def timeit(func, iterations, *args, _verbose=0, **kwargs):
t0 = time()
for _ in range(iterations):
func(*args, **kwargs)
print(end='.'*int(_verbose))
print("Time/iter: %.4f sec" % ((time() - t0) / iterations))
def make_model_small(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(128, 40, strides=4, padding='same')(ipt)
x = GlobalAveragePooling1D()(x)
x = Dropout(0.5)(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_medium(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = ipt
for filters in [64, 128, 256, 256, 128, 64]:
x = Conv1D(filters, 20, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_large(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(64, 400, strides=4, padding='valid')(ipt)
x = Conv1D(128, 200, strides=1, padding='valid')(x)
for _ in range(40):
x = Conv1D(256, 12, strides=1, padding='same')(x)
x = Conv1D(512, 20, strides=2, padding='valid')(x)
x = Conv1D(1028, 10, strides=2, padding='valid')(x)
x = Conv1D(256, 1, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_data(batch_shape):
return np.random.randn(*batch_shape), \
np.random.randint(0, 2, (batch_shape[0], 1))
def make_data_tf(batch_shape, n_batches, iters):
data = np.random.randn(n_batches, *batch_shape),
trgt = np.random.randint(0, 2, (n_batches, batch_shape[0], 1))
return tf.data.Dataset.from_tensor_slices((data, trgt))
batch_shape_small = (32, 140, 30)
batch_shape_medium = (32, 1400, 30)
batch_shape_large = (32, 14000, 30)
batch_shapes = batch_shape_small, batch_shape_medium, batch_shape_large
make_model_fns = make_model_small, make_model_medium, make_model_large
iterations = [200, 100, 50]
shape_names = ["Small data", "Medium data", "Large data"]
model_names = ["Small model", "Medium model", "Large model"]
def test_all(fit=False, tf_dataset=False):
for model_fn, model_name, iters in zip(make_model_fns, model_names, iterations):
for batch_shape, shape_name in zip(batch_shapes, shape_names):
if (model_fn is make_model_large) and (batch_shape == batch_shape_small):
continue
reset_seeds(reset_graph_with_backend=K)
if tf_dataset:
data = make_data_tf(batch_shape, iters, iters)
else:
data = make_data(batch_shape)
model = model_fn(batch_shape)
if fit:
if tf_dataset:
model.train_on_batch(data.take(1))
t0 = time()
model.fit(data, steps_per_epoch=iters)
print("Time/iter: %.4f sec" % ((time() - t0) / iters))
else:
model.train_on_batch(*data)
timeit(model.fit, iters, *data, _verbose=1, verbose=0)
else:
model.train_on_batch(*data)
timeit(model.train_on_batch, iters, *data, _verbose=1)
cprint(">> {}, {} done <<\n".format(model_name, shape_name), 'blue')
del model
test_all(fit=True, tf_dataset=False)