1.3 张量:排程室里的货物规格 本节摘要:张量是 TensorFlow 中数据的唯一载体,本质是带三要素——维度、形状、类型——的多维数组。本节讲透三要素如何决定一个张量能否被某个算子调度:形状不符工单会退回,类型不符会被静默拒绝,广播规则决定不同规格货物能否同槽作业。掌握"规格思维"后,一半以上的入门期报错可以在发生前被预判。 读完你应当能做到 阅读完本节,你应当能够: 现场创建标量到四维的各类张量,并用排程语言描述每种规格的典型用途; 用 reshape、expanddims、transpose 等操作在不复制数据语义的前提下改变规格; 独立判断任意二元运算的广播结果形状,并指出何时广播是错的; 区分 tf.Tensor 的不可变性与 tf.Variable 的可写性(后者在 1.
本节摘要:张量是 TensorFlow 中数据的唯一载体,本质是带三要素——维度、形状、类型——的多维数组。本节讲透三要素如何决定一个张量能否被某个算子调度:形状不符工单会退回,类型不符会被静默拒绝,广播规则决定不同规格货物能否同槽作业。掌握"规格思维"后,一半以上的入门期报错可以在发生前被预判。
阅读完本节,你应当能够:
把一张彩色图片存进内存,最自然的结构是三维数组:高、宽、通道。但训练从来不是一张一张喂——GPU 最擅长的是对成批数据同时做同样的运算,所以前面还要再加一个"批次"维度,变成四维:批次、高、宽、通道。文本呢?一批影评文本是四维:批次、句子长度、……再乘上词向量的维度。时间序列、图结构同理,全都堆在"批次、序列、特征"这套维度骨架上。张量的维度不是数学炫技,而是让 GPU 用一次并行调度吃掉一整批数据的前提。

每个张量都有三要素。维度(rank)是轴的个数;形状(shape)是每根轴的长度;类型(dtype)是元素类型。规格决定命运:算子接单时先查规格,不匹配就退单。
import tensorflow as tf import numpy as np # 创建各规格张量 s = tf.constant(3.14) # 标量:0 维,shape=() v = tf.constant([1.0, 2.0, 3.0]) # 向量:1 维 m = tf.constant([[1, 2], [3, 4]]) # 矩阵:2 维 img_batch = tf.zeros([4, 200, 40, 3]) # 图像批次:4 维 print(s.shape, v.shape, m.shape, img_batch.shape) # 输出:() (3,) (2, 2) (4, 200, 40, 3) print(img_batch.dtype) # 输出:<dtype: 'float32'> # 从 NumPy 转入:常用操作,np 数组与 tf 张量可互转 arr = np.arange(6).reshape(2, 3) t = tf.convert_to_tensor(arr) print(t) # 输出:tf.Tensor( # [[0 1 2] # [3 4 5]], shape=(2, 3), dtype=int64)
注意最后输出的 dtype 是 int64——从 NumPy 转入时类型跟着数组走,而 NumPy 默认整型是 64 位。深度学习里几乎总是想把它压成 float32,类型不匹配是"看代码没问题、一跑就报错"的经典来源。规格操作三件套是 reshape、expand_dims、transpose:
x = tf.constant([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) r = tf.reshape(x, [3, 2]) # 重排:元素总数不变 6 个 print(r.shape) # (3, 2) e = tf.expand_dims(x, axis=0) # 在轴 0 前插一根新轴 print(e.shape) # (1, 2, 3) —— 批次维就这样造出来 sq = tf.squeeze(e) # 去掉长度为 1 的轴 print(sq.shape) # (2, 3) tr = tf.transpose(x) # 转置:交换轴 print(tr) # 输出:tf.Tensor( # [[1 4] # [2 5] # [3 6]], shape=(3, 2), dtype=int32)
reshape 是"换包装不换货":元素总个数不变,只是重新解释轴的切分。深度学习里最常见的是把 (batch, 28, 28, 3) 的图像批次压平成 (batch, 2352) 喂给全连接层——Flatten 层干的就是这件事。
两个形状不同的张量做运算时,TensorFlow 按广播规则自动对齐:从最后一维向前逐维比较,两个维度相等、或其中一方为 1、或一方缺失,就兼容;长度为 1 的维度被虚拟复制到对方长度。规则本身简单,难的是预判——练一遍:
a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # (2, 3) b = tf.constant([10.0, 20.0, 30.0]) # (3,) print(a + b) # (3,) 被广播成 (2, 3),每行加同一组数: # tf.Tensor( # [[11. 22. 33.] # [14. 25. 36.]], shape=(2, 3), dtype=float32) c = tf.constant([[1.0], [2.0]]) # (2, 1) print(a * c) # (2, 1) 广播成 (2, 3),第一行乘 1、第二行乘 2: # tf.Tensor( # [[ 1. 2. 3.] # [ 8. 10. 12.]], shape=(2, 3), dtype=float32) # 预判练习:以下两个是否可广播? # (4, 1, 3) 与 (2, 3) → 可,结果 (4, 2, 3) # (4, 2) 与 (3,) → 否,最后一维 2 对 3 不匹配,报错
广播省去了手工平铺的内存开销,但它也是沉默的错误放大器:形状差一位却能广播成功时,计算会"正常"跑完,产出错误结果。防错习惯是算完立刻断言形状:
y = a * c tf.debugging.assert_equal(tf.shape(y), [2, 3]) print("shape ok") # 输出:shape ok # 形状不符时会抛 InvalidArgumentError,把规格错误拦在源头
批次维度放第 0 轴是 TensorFlow 的全局约定,数据管道(第 2 章)产出的每一个批次都遵循它。类型上,训练默认 float32;混合精度训练时输入保持 float32、部分运算切到 float16 换吞吐,属于进阶调优。有个细节值得记住:Python 标量与张量混合运算没问题,TensorFlow 会自动包装,但在 @tf.function 里用 Python 标量当循环变量会把循环展开进图,改用 tf.range 才能作为动态张量循环——这是规格与执行模式交叉的一个预告,1.5 节详谈。
⚠️ 常见坑:
tf.constant(3.14)与tf.constant([3.14])是 0 维与 1 维两个不同规格的张量,某些算子只接受其一。报错信息里出现 shape 不匹配时,先打印两边 shape 再动手改,比肉眼盯着代码猜快得多。
💡 关键直觉:把 shape 当成张量的"身份证"。写复杂模型时每过一层就注释一下当前 shape,注释到哪个环节对不上,bug 就在哪个环节——这套笨办法能排掉九成维度类错误。
下节看消费这些货物的最小工单——算子。