使用 Tensorflow.js 和 tf.Tensor 处理大数据的最佳方法是什么?

What is the best way to handle large data with Tensorflow.js and tf.Tensor?

问题

我正在使用 tf.Tensortf.concat() 来处理大量训练数据, 我发现连续使用 tf.concat() 会变慢。 将大数据从文件加载到 tf.Tensor 的最佳方法是什么?

背景

我认为在Javascript中按数组处理数据是常见的方式。 要实现这一目标,请执行以下粗略步骤。

从文件加载数据到数组的步骤

  1. 从文件中读取行
  2. 将行解析为 Javascript 的对象
  3. 通过Array.push()
  4. 将该对象添加到数组
  5. 读完一行后,我们可以用for循环使用那个数组。

所以我想我可以像上面那样使用 tf.concat()

将数据从文件加载到 tf.Tensor

的步骤
  1. 从文件中读取行
  2. 将行解析为 Javascript 的对象
  3. 将对象解析为 tf.Tensor
  4. 将张量添加到原始张量 tf.concat()
  5. 读完一行后,我们可以使用tf.Tensor

一些代码

这里有一些代码可以测量 Array.push()tf.concat()

的速度
import * as tf from "@tensorflow/tfjs"

let t = tf.tensor1d([1])
let addT = tf.tensor1d([2])

console.time()
for (let idx = 0; idx < 50000; idx++) {
    if (idx % 1000 == 0) {
        console.timeEnd()
        console.time()
        console.log(idx)
    }
    t = tf.tidy(() => t.concat(addT))
}


let arr = []
let addA = 1
console.time()
for (let idx = 0; idx < 50000; idx++) {
    if (idx % 1000 == 0) {
        console.timeEnd()
        console.time()
        console.log(idx)
    }
    arr.push(addA)
}

测量

我们可以在 Array.push() 上看到稳定的进程, 但在 tf.concat()

上变慢了

对于tf.concat()

default: 0.150ms
0
default: 68.725ms
1000
default: 62.922ms
2000
default: 23.199ms
3000
default: 21.093ms
4000
default: 27.808ms
5000
default: 39.689ms
6000
default: 34.798ms
7000
default: 45.502ms
8000
default: 94.526ms
9000
default: 51.996ms
10000
default: 76.529ms
11000
default: 83.662ms
12000
default: 45.730ms
13000
default: 89.119ms
14000
default: 49.171ms
15000
default: 48.555ms
16000
default: 55.686ms
17000
default: 54.857ms
18000
default: 54.801ms
19000
default: 55.312ms
20000
default: 65.760ms

对于Array.push()

default: 0.009ms
0
default: 0.388ms
1000
default: 0.340ms
2000
default: 0.333ms
3000
default: 0.317ms
4000
default: 0.330ms
5000
default: 0.289ms
6000
default: 0.299ms
7000
default: 0.291ms
8000
default: 0.320ms
9000
default: 0.284ms
10000
default: 0.343ms
11000
default: 0.327ms
12000
default: 0.317ms
13000
default: 0.329ms
14000
default: 0.307ms
15000
default: 0.218ms
16000
default: 0.193ms
17000
default: 0.234ms
18000
default: 1.943ms
19000
default: 0.164ms
20000
default: 0.148ms

虽然 tf.concatArray.push 函数的外观和行为相似,但有一个很大的区别:

  • tf.concat 从输入
  • 创建一个 新张量
  • Array.push 将输入添加到第一个数组

例子

tf.concat

const a = tf.tensor1d([1, 2]);
const b = tf.tensor1d([3]);
const c = tf.concat([a, b]);

a.print(); // Result: Tensor [1, 2]
b.print(); // Result: Tensor [3]
c.print(); // Result: Tensor [1, 2, 3]

结果变量 c 是一个新的张量,而 ab 没有改变。

Array.push

const a = [1,2];
a.push(3);

console.log(a); // Result: [1,2,3]

这里直接改变量a

对运行时的影响

对于运行速度,这意味着 tf.concat 在添加输入之前将所有张量值复制到一个新的张量。显然,需要复制的数组越大,花费的时间就越多。与此相反,Array.push 不会创建数组的副本,因此无论数组有多大,运行时都或多或少相同。

请注意,这是 "by design",因为张量是不可变的,因此对现有张量的每次操作都会创建一个新的张量。引自 docs:

Tensors are immutable, so all operations always return new Tensors and never modify input Tensors.

因此,如果您需要从输入数据创建一个大张量,建议首先从您的文件中读取所有数据并将其与 "vanilla" JavaScript 函数合并,然后再从中创建张量.

处理内存太大的数据

如果您的数据集太大以至于由于内存限制需要分块处理,您有两个选择:

  1. 使用trainOnBatch函数
  2. 使用数据集生成器

选项 1:trainOnBatch

trainOnBatch 函数允许对一批数据进行训练,而不是使用完整的数据集。因此,您可以在训练代码之前将代码分成合理的批次,这样就不必一次将所有数据合并在一起。

选项 2:数据集生成器

另一个答案已经涵盖了基础知识。这将允许您使用 JavaScript generator function 来准备数据。我建议使用生成器语法而不是迭代器工厂(在另一个答案中使用),因为它是更现代的 JavaScript 语法。

示例(取自docs):

function* dataGenerator() {
  const numElements = 10;
  let index = 0;
  while (index < numElements) {
    const x = index;
    index++;
    yield x;
  }
}

const ds = tf.data.generator(dataGenerator);

然后您可以使用 fitDataset 函数来训练您的模型。

虽然没有一种方法可以创建张量,但问题的答案在于如何处理创建的张量。

性能

张量是不可变的,因此每次调用 tf.concat 都会创建一个新的张量。

let x = tf.tensor1d([2]);
console.log(tf.memory()) // "numTensors": 1
const y = tf.tensor1d([3])
x = tf.concat([x, y])
console.log(tf.memory()) // "numTensors": 3, 
<html>
  <head>
    <!-- Load TensorFlow.js -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.14.1"> </script>
  </head>

  <body>
  </body>
</html>

正如我们从上面的代码片段中看到的,调用 tf.concat 时创建的张量数量是 3 而不是 2tf.tidy 确实会处理未使用的张量。但是随着创建的张量越来越大,这种创建和处理张量的操作将变得最昂贵。这既是内存消耗又是计算的问题,因为创建新的张量将始终委托给后端。


从大数据创建张量

既然了解了性能问题,那么最好的方法是什么?

  • 在js中创建整个数组,当整个数组完成后,再创建张量。
for (i= 0; i < data.length; i++) {
  // fill array x
  x.push(dataValue)
}
// create the tensor
tf.tensor(x)

尽管这是微不足道的解决方案,但并非总是可行。因为创建一个数组会将数据保存在内存中,我们可以很容易地 运行 内存不足的大数据条目。因此有时,最好不要创建整个 javascript 数组来创建数组块并从这些数组块创建张量,并在创建张量后立即开始处理这些张量。如有必要,可以再次使用 tf.concat 合并块张量。但它可能并不总是必需的。

例如,我们可以使用张量块重复调用 model.fit() 而不是使用可能需要很长时间才能创建的大张量调用一次。在这种情况下,不需要连接块张量。

  • 如果可能,使用 tf.data 创建数据集。这是理想的解决方案,如果我们下一步要用数据拟合模型。
function makeIterator() {

  const iterator = {
    next: () => {
      let result;
      if (index < data.length) {
        result = {value: dataValue, done: false};
        index++;
        return result;
      }
      return {value: dataValue, done: true};
    }
  };
  return iterator;
}
const ds = tf.data.generator(makeIterator);

使用tf.data的优点是整个数据集是在model.fit调用期间需要时批量创建的。