2、核心概念:张量(Tensor)的创建、操作与运算、变量与内存管理

好,咱们进入正题。张量,说白了就是 TensorFlow.js 里的「数据容器」。你想想看,不管你是处理图片、文字还是音频,最终都得变成数字塞进这个容器里。我刚开始接触时,总觉得这玩意儿不就是个数组吗?后来踩了坑才明白——它比数组聪明得多。

2.1 张量的创建:从零开始造数据

创建张量,我习惯用 tf.tensor() 这个万能方法。但说实话,日常开发中我更常用它的几个「快捷方式」。

核心原则: 张量一旦创建,数据就驻留在 GPU 或 WebGL 上下文中。你没法直接修改它的值,只能通过运算生成新张量。

2.1.1 从普通数组创建

// 一维张量(向量)
const vec = tf.tensor([1, 2, 3, 4, 5]);
vec.print(); // [1, 2, 3, 4, 5]

// 二维张量(矩阵)
const mat = tf.tensor([[1, 2], [3, 4], [5, 6]]);
mat.print(); 
// [[1, 2],
//  [3, 4],
//  [5, 6]]

// 三维张量(比如一张 RGB 小图)
const imgData = tf.tensor([
  [[255, 0, 0], [0, 255, 0]],
  [[0, 0, 255], [255, 255, 0]]
]);
imgData.print(); // shape: [2, 2, 3]

嗯,这里要注意:tf.tensor() 会自动推断数据类型。如果你需要整数型,记得用 tf.tensor(..., 'int32')。我曾经在模型推理时因为默认浮点型导致精度对不上,排查了半天……

2.1.2 专用创建函数

函数 说明 示例
tf.scalar() 创建零维张量(标量) tf.scalar(3.14)
tf.zeros() 全零张量 tf.zeros([3, 4])
tf.ones() 全一张量 tf.ones([2, 2])
tf.fill() 填充指定值 tf.fill([2, 3], 7)
tf.randomNormal() 正态分布随机数 tf.randomNormal([100])
tf.range() 生成等差数列 tf.range(0, 10, 2)
我的习惯: 初始化权重时用 tf.randomNormal(),做测试数据时用 tf.range()。这样一眼就能看出数据对不对。

2.2 张量的操作:形状变换与数据提取

创建完张量,下一步就是操作它。说白了,就是「揉捏」这个数据块。

2.2.1 形状操作

const a = tf.tensor([[1, 2, 3], [4, 5, 6]]);
console.log(a.shape); // [2, 3]

// 重塑形状
const b = a.reshape([3, 2]);
b.print(); 
// [[1, 2],
//  [3, 4],
//  [5, 6]]

// 展平成一维
const c = a.flatten();
c.print(); // [1, 2, 3, 4, 5, 6]

这里有个坑:reshape 不会改变数据在内存中的排列顺序。它只是换了个「视角」看数据。我曾经以为 reshape 会重新排列数据,结果模型训练出来的结果完全不对……

2.2.2 切片与拼接

const data = tf.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);

// 切片:取第1行到第2行,第0列到第1列
const slice = data.slice([1, 0], [2, 2]);
slice.print(); 
// [[4, 5],
//  [7, 8]]

// 拼接
const a = tf.tensor([[1, 2]]);
const b = tf.tensor([[3, 4]]);
const concat = tf.concat([a, b], 0); // 按行拼接
concat.print(); // [[1, 2], [3, 4]]
注意: 切片操作返回的是原张量的「视图」,不是副本。修改视图会影响原张量。如果你需要独立副本,记得调用 .clone()

2.3 张量的运算:数学才是灵魂

张量运算分两类:逐元素运算和矩阵运算。我刚开始时总搞混,后来记住一句话——「逐元素就是对应位置算,矩阵运算看维度」。

2.3.1 逐元素运算

const a = tf.tensor([1, 2, 3]);
const b = tf.tensor([4, 5, 6]);

// 加法
a.add(b).print(); // [5, 7, 9]

// 乘法(逐元素乘)
a.mul(b).print(); // [4, 10, 18]

// 平方
a.square().print(); // [1, 4, 9]

// 激活函数
a.relu().print(); // [1, 2, 3](负数会被截断为0)

2.3.2 矩阵运算

const matA = tf.tensor([[1, 2], [3, 4]]);
const matB = tf.tensor([[5, 6], [7, 8]]);

// 矩阵乘法
const result = matA.matMul(matB);
result.print();
// [[19, 22],
//  [43, 50]]

// 转置
const transposed = matA.transpose();
transposed.print();
// [[1, 3],
//  [2, 4]]
避坑指南: 我曾经在写全连接层时,把 matMulmul 搞混了。结果模型收敛速度奇慢,损失函数死活降不下去。记住:matMul 是矩阵乘法,mul 是逐元素乘,完全两码事。

2.4 变量与内存管理:别让你的浏览器崩了

这是很多新手容易忽略的地方。TensorFlow.js 跑在浏览器里,内存有限。如果你不注意管理,页面会越来越卡,最后直接崩溃。我踩过这个坑,那次演示时页面卡死,场面一度非常尴尬……

2.4.1 变量(Variable)

张量是不可变的,但模型训练需要更新参数。这时候就需要 tf.Variable

// 创建变量
const w = tf.variable(tf.randomNormal([2, 3]));
console.log(w.value()); // 获取当前值

// 更新变量
w.assign(tf.ones([2, 3]));
console.log(w.value()); // 全1矩阵

变量说白了就是「可变的张量」。模型训练时,优化器会调用 assign 来更新权重。我个人习惯把所有可训练参数都声明为变量,这样一目了然。

2.4.2 内存管理:手动释放的艺术

TensorFlow.js 默认会跟踪所有张量。如果你创建了张量却不释放,内存会越堆越多。

// 方法一:手动 dispose
const a = tf.tensor([1, 2, 3]);
const b = a.square();
// 用完后释放
a.dispose();
b.dispose();

// 方法二:使用 tf.tidy(推荐)
const result = tf.tidy(() => {
  const a = tf.tensor([1, 2, 3]);
  const b = a.square();
  return b; // 只保留 b,a 自动释放
});
// 此时 a 已被释放,result 保留
我的经验: 在循环或频繁调用的函数里,一定要用 tf.tidy 包裹。我曾经写过一个数据增强函数,每张图片生成 10 个变体,没用 tidy 导致内存暴涨,浏览器直接崩溃。加上 tidy 后,内存稳定在 200MB 以内。

2.4.3 内存泄漏排查

// 查看当前内存状态
console.log(tf.memory());

// 输出示例:
// {
//   numTensors: 15,    // 当前张量数量
//   numDataBuffers: 10, // 数据缓冲区数量
//   numBytes: 4096     // 占用字节数
// }

如果你发现 numTensors 只增不减,那肯定有地方没释放。我排查时习惯在关键位置打印 tf.memory(),看看到底是哪个环节出了问题。

重要提醒: 不要依赖垃圾回收!JavaScript 的 GC 不会主动回收 WebGL 纹理。你必须手动 dispose() 或使用 tf.tidy()。这是浏览器端机器学习最容易被忽视的性能杀手。

2.5 实战小技巧:我踩过的坑

  • 数据类型不匹配: 模型要求 float32,你传了 int32,运算会报错。统一用 tf.cast() 转换。
  • 形状不对: 矩阵乘法时,A 的列数必须等于 B 的行数。我习惯在运算前打印 .shape 确认。
  • 忘记释放: 写一个工具函数,每次创建张量后自动注册释放逻辑。我写了个 createTrackedTensor 函数,省了不少事。
  • 变量初始化: 不要全零初始化,会导致梯度消失。用 tf.randomNormal 或 Xavier 初始化。

好了,张量的核心概念就这些。说白了就是三件事:创建数据、操作数据、管理内存。下一章咱们会把这些知识用到实际模型里,到时候你会发现——嗯,基础打牢了,后面就顺了。