我在tensorflow中实现了一个简单的训练器类。我正在运行一些实验来检查代码性能,但是在了解tf.data.Dataset和tf.function的作用时遇到了问题。

在下面的内容中,我将介绍我已经进行的测试,最后,我会得到一些关于结果的疑问。

配置:Intel i3 cpu,tensorflow-cpu 2.1

class Trainer:
    def __init__(self, model, optimizer, loss):
        self.model = model
        self.loss_function = loss
        self.optimizer = optimizer

    @tf.function
    def train_step(self, inputs, targets):
        with tf.GradientTape() as tape:
            predictions = self.model(inputs)
            loss = self.loss_function(targets, predictions)
        gradients = tape.gradient(loss, self.model.trainable_variables)
        self.optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
        return loss

    # fit using dataset
    @tf.function
    def fit0(self, dataset, epochs):
        for epoch in tf.range(epochs):
            for input_batch, target_batch in dataset:
                self.train_step(input_batch, target_batch)

    # fit using list of tensors
    @tf.function
    def fit1(self, inputs, targets, epochs):
        for epoch in tf.range(epochs):
            for input_batch, target_batch in zip(inputs, targets):
                self.train_step(input_batch, target_batch)


在下面的代码中,train_step将始终包装在tf.function中。

fit0,fit1将在有和没有tf.function的情况下进行测试。

这里运行测试的代码:

input_size = 10000
batch_size = 100
q = input_size // batch_size

# create random inputs (x) and outputs (y)
x = tf.random.normal((input_size, 1), dtype=tf.float32)
y = tf.random.normal((input_size, 1), dtype=tf.float32)

splits = tf.fill([q, ], batch_size)

# create a list of tensors rappresenting batches
x_list = tf.split(x, splits)
y_list = tf.split(y, splits)

# create datasets in the different ways
dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size)
dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list)))

# model definition
model = tf.keras.Sequential([
    tf.keras.layers.Dense(20, activation='tanh', input_shape=(1,)),
    tf.keras.layers.Dense(1, activation='linear')])

# trainer initialization
trainer = Trainer(model=model, optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.MeanSquaredError())

# first run to perform initializations
time0 = time.time()
trainer.fit0(dataset=dataset0, epochs=tf.constant(1, dtype=tf.int32))
time0 = time.time() - time0

time1 = time.time()
trainer.fit0(dataset=dataset1, epochs=tf.constant(1, dtype=tf.int32))
time1 = time.time() - time1

time2 = time.time()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(1, dtype=tf.int32))
time2 = time.time() - time2

print("first fit0 with dataset0 took {} seconds".format(time0))
print("first fit0 with dataset1 took {} seconds".format(time1))
print("first fit1 with tensorlist took {} seconds".format(time2))

# measure performances
time0 = time.time()
trainer.fit0(dataset=dataset0, epochs=tf.constant(100, dtype=tf.int32))
time0 = time.time() - time0

time1 = time.time()
trainer.fit0(dataset=dataset1, epochs=tf.constant(100, dtype=tf.int32))
time1 = time.time() - time1

time2 = time.time()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(100, dtype=tf.int32))
time2 = time.time() - time2

print("fit0 with dataset0 took {} seconds".format(time0))
print("fit0 with dataset1 took {} seconds".format(time1))
print("fit1 with tensorlist took {} seconds".format(time2))


这里的测试结果:

第一个测试是对100个批次(每批次100个样品)进行测试。


  input_size = 10000
  batch_size = 100
  
  没有@ tf.function:
  数据集0的第一个fit0花费了0.9953532218933105秒
  数据集1的第一个fit0花费了0.07995295524597168秒
  张量表的第一次fit1用了0.05196571350097656秒
  使用数据集0的fit0花费了10.46957802772522秒
  使用数据集1的fit0花费了7.822799205780029秒
  tensorlist的fit1用了4.650130748748779秒
  
  使用@ tf.function:
  数据集0的第一个fit0用了1.4042332172393799秒
  与dataset1的第一个fit0用了0.46071624755859375秒
  与张量表的第一次fit1用了7.3524699211120605秒
  使用数据集0的fit0花费了15.077088832855225秒
  使用数据集1的fit0花费了9.136569738388062秒
  tensorlist的fit1用了2.1366817951202393秒


第二个是一批100000个样本。


  input_size = 100000
  batch_size = 100000
  
  没有@ tf.function:
  与dataset0的第一个fit0用了1.1792669296264648秒
  数据集1的第一个fit0花费了0.027983427047729492秒
  张量表的首次拟合1花费了0.020987749099731445秒
  使用数据集0的fit0用了28.71895956993103秒
  具有数据集1的fit0花费了2.730872869491577秒
  tensorlist的fit1用了2.194814682006836秒
  
  使用@ tf.function:
  数据集0的第一个fit0用了1.5979444980621338秒
  数据集1的首次拟合0花费了0.4557182788848877秒
  与张量表的第一次fit1用了0.3708038330078125秒
  使用数据集0的fit0花费了36.43854784965515秒
  具有数据集1的fit0花费了9.819332122802734秒
  tensorlist的fit1用了2.1136972904205322秒


问题:


为什么用tf.function包装时,tf.data.Dataset的性能最差?
即使dataset0和dataset1在功能上等效。两者之间的内在区别是什么?为什么dataset1的性能优于dataset0?
具有tf.function的fit1具有最佳的长期性能。


使用tf.data.Dataset是否可以达到相同的性能?
为什么要花这么多时间进行初始化?
当使用100个批次时,第一次运行花费了7.3524699211120605秒,并且此时间通过增加批次数而增加。
我猜是因为亲笔签名正在创建一个更大的图,展开了不同批次的计算。但是我看不到任何并行化的机会,因为每个批次都取决于前一个的结果。

最佳答案

我获得了不错的性能改进,代码和结果如下所示。
但是,我只能部分回答这些问题,特别是第二个问题仍然没有解决。

配置:Intel i3 cpu,tensorflow-cpu 2.1
这是功能fit0的改进代码,其余Trainer类未更改:

# fit using dataset
@tf.function
def fit0(self, dataset, epochs, batches, unroll=1):
    tf.assert_equal(tf.is_tensor(unroll), False, "unroll must be a python variable.")
    tf.assert_equal(tf.math.floormod(batches, unroll), tf.constant(0), "unroll must be a divisor of batches.")

    entries = epochs * batches / unroll
    it = iter(dataset)

    for entry in tf.range(entries):
        # this loop gets unrolled if unroll
        # is python variable, not a tensor.
        for _ in range(unroll):
            input_batch, target_batch = next(it)
            self.train_step(input_batch, target_batch)


这里运行测试的代码:

input_size = 100000
batch_size = 100
num_epochs = 100
num_unroll = 5

num_batches = input_size // batch_size

# create random inputs (x) and outputs (y)
x = tf.random.normal((input_size, 1), dtype=tf.float32)
y = tf.random.normal((input_size, 1), dtype=tf.float32)

splits = tf.fill([num_batches, ], batch_size)
x_list, y_list = tf.split(x, splits), tf.split(y, splits)

# create dataset
dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size).cache().prefetch(1).repeat(num_epochs)
dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list))).cache().prefetch(1).repeat(num_epochs)

# model definition
model = tf.keras.Sequential([
    tf.keras.layers.Dense(20, activation='tanh', input_shape=(1,)),
    tf.keras.layers.Dense(1, activation='linear')])

# trainer initialization
trainer = Trainer(model=model, optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.MeanSquaredError())

# first run to perform initializations
time0 = time.perf_counter()
trainer.fit0(
    dataset=dataset0,
    epochs=tf.constant(1, dtype=tf.int32),
    batches=tf.constant(num_batches, dtype=tf.int32),
    unroll=num_unroll)
time0 = time.perf_counter() - time0

time1 = time.perf_counter()
trainer.fit0(
    dataset=dataset1,
    epochs=tf.constant(1, dtype=tf.int32),
    batches=tf.constant(num_batches, dtype=tf.int32),
    unroll=num_unroll)
time1 = time.perf_counter() - time1

time2 = time.perf_counter()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(1, dtype=tf.int32))
time2 = time.perf_counter() - time2

print("first fit0 with dataset0 took {} seconds".format(time0))
print("first fit0 with dataset1 took {} seconds".format(time1))
print("first fit1 with tensorlist took {} seconds".format(time2))

# measure performances
time0 = time.perf_counter()
trainer.fit0(
    dataset=dataset0,
    epochs=tf.constant(num_epochs, dtype=tf.int32),
    batches=tf.constant(num_batches, dtype=tf.int32),
    unroll=num_unroll)
time0 = time.perf_counter() - time0

time1 = time.perf_counter()
trainer.fit0(
    dataset=dataset1,
    epochs=tf.constant(num_epochs, dtype=tf.int32),
    batches=tf.constant(num_batches, dtype=tf.int32),
    unroll=num_unroll)
time1 = time.perf_counter() - time1

time2 = time.perf_counter()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(num_epochs, dtype=tf.int32))
time2 = time.perf_counter() - time2

print("fit0 with dataset0 took {} seconds".format(time0))
print("fit0 with dataset1 took {} seconds".format(time1))
print("fit1 with tensorlist took {} seconds".format(time2))



  
  为什么用tf.function包装时,tf.data.Dataset的性能最差?
  


我不知道引擎盖下到底发生了什么,但是可以通过替换来解决:

dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size)
dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list)))


使用这个新的数据集,其中还包括epoch以及使用缓存和预取。

dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size).cache().prefetch(1).repeat(num_epochs)
dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list))).cache().prefetch(1).repeat(num_epochs)


可以在here中找到更多信息。

我测试了带有和不带有tf.function的fit0,fit1,但是通过使用tf.function我始终可以获得更好的性能,因此仅显示后者。

使用的input_size大10倍。这里的测试结果:

第一个测试是对1000个批次(每个100个样本)进行测试。
请注意,与num_unroll = 1相比,num_unroll = 5可以提高性能。将num_unroll> 5设置不会带来任何进一步的改进。


  input_size = 100000
  batch_size = 100
  num_epochs = 100
  num_unroll = 5
  
  数据集0的第一个fit0用了2.2224882999999993秒
  数据集1的首次拟合0花费了0.804360700000001秒
  张量表的第一次fit1花费了88.2123332秒
  使用数据集0的fit0花费了35.27911590000001秒
  具有数据集1的fit0花费了20.370243099999982秒
  具有张量表的fit1花费了23.66727979999999秒


第二个是带有1批次的1000000个样本。


  input_size = 100000
  batch_size = 100000
  
  input_size = 1000000
  batch_size = 1000000
  num_epochs = 100
  num_unroll = 1
  
  数据集0的第一个fit0用了4.3616363秒
  数据集1的首次拟合0花费了0.7977632000000003秒
  张量表的第一次fit1用了0.7329889000000005秒
  使用数据集0的fit0花费了21.131495899999997秒
  与数据集1的fit0花费了19.915148600000002秒
  tensorlist的fit1用了19.817472700000003秒


以上结果可以回答以下问题:


  
  具有tf.function的fit1具有最佳的长期性能。
  
  
  使用tf.data.Dataset是否可以达到相同的性能?
  为什么要花这么多时间进行初始化?
  当使用100批次时,第一次运行耗时7.3524699211120605秒,因此
  通过增加批数来增加时间。我想是
  因为签名会创建更大的图形,因此展开了
  计算不同的批次。我看不到任何机会
  但是并行化,因为每个批次都取决于结果
  前一个。
  
  


通过检查TensorBoard上图的结构,可以很容易地看到,在fit1函数上使用自动签名,可以通过完全展开循环来创建非常大的图。
这样可以提供更好的性能,但是创建图形的时间很长,并且很可能会过度使用内存,这使得它无法用于更复杂的问题。
但是,如上所述,使用tf.data.Dataset可以实现相同的性能,而只需几个展开的循环,从而可以改善图形的大小。

关于python - 使用tf.data.Dataset的Tensorflow性能下降,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/59704981/

10-12 18:18