2、核心概念:张量(Tensor)的创建、操作与运算、变量与内存管理
好,咱们进入正题。张量,说白了就是 TensorFlow.js 里的「数据容器」。你想想看,不管你是处理图片、文字还是音频,最终都得变成数字塞进这个容器里。我刚开始接触时,总觉得这玩意儿不就是个数组吗?后来踩了坑才明白——它比数组聪明得多。
2.1 张量的创建:从零开始造数据
创建张量,我习惯用 tf.tensor() 这个万能方法。但说实话,日常开发中我更常用它的几个「快捷方式」。
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]]
matMul 和 mul 搞混了。结果模型收敛速度奇慢,损失函数死活降不下去。记住: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(),看看到底是哪个环节出了问题。
dispose() 或使用 tf.tidy()。这是浏览器端机器学习最容易被忽视的性能杀手。
2.5 实战小技巧:我踩过的坑
- 数据类型不匹配: 模型要求 float32,你传了 int32,运算会报错。统一用
tf.cast()转换。 - 形状不对: 矩阵乘法时,A 的列数必须等于 B 的行数。我习惯在运算前打印
.shape确认。 - 忘记释放: 写一个工具函数,每次创建张量后自动注册释放逻辑。我写了个
createTrackedTensor函数,省了不少事。 - 变量初始化: 不要全零初始化,会导致梯度消失。用
tf.randomNormal或 Xavier 初始化。
好了,张量的核心概念就这些。说白了就是三件事:创建数据、操作数据、管理内存。下一章咱们会把这些知识用到实际模型里,到时候你会发现——嗯,基础打牢了,后面就顺了。