
文章目录
1. 课前导读
1.1 本节课学习目标
- 深刻理解张量(Tensor)作为TensorFlow核心数据载体的本质:多维数组。
- 掌握张量的三个基本属性:形状(shape)、数据类型(dtype)、设备(device)。
- 学会创建不同类型的张量(常量、变量、随机、序列)。
- 熟练进行数据类型转换与形状变换(reshape、转置、维度扩展/压缩)。
- 完全理解广播机制的原理与规则,能够正确预测广播结果。
- 能够区分
tf.Tensor与tf.Variable,并知道何时使用变量。
1.2 知识重难点
| 类别 | 内容 |
|---|---|
| 重点 | 张量的形状与维度操作(reshape、transpose、expand_dims、squeeze);广播机制的三个核心规则 |
| 难点 | 高阶张量(4维及以上)的形状变换思维;广播时内存不复制但逻辑扩展的底层机制;tf.Variable的可变性及其与tf.Tensor的转换 |
| 易混淆点 | reshape与transpose的区别;tf.reshape与tf.constant中-1占位符的含义;np.array与tf.Tensor的互操作时的数据类型隐式转换 |
1.3 学习前置条件
- 已按照第4课完成TensorFlow 2.x安装,并能够成功导入
tensorflow。 - 具备基本的NumPy数组操作知识(第2课内容),因张量与NumPy数组高度相似。
- 了解Python的基本数据类型(int、float、bool)。
1.4 学完可掌握能力
- 能够为神经网络中的输入数据(如图像、文本序列)创建合适形状的张量。
- 进行数据预处理时灵活调整张量维度(例如从
(H,W)扩展到(1,H,W,1))。 - 利用广播机制简化代码,避免手动复制数据造成的额外内存开销。
- 正确选择
tf.Tensor与tf.Variable来存储模型参数或中间结果。 - 独立排查因形状不匹配或广播误解导致的运行时错误。
1.5 行业应用场景
- 图像数据表示:四维张量
(batch, height, width, channels)。 - 文本序列处理:二维张量
(batch, seq_len)存储词索引。 - 模型参数存储:所有可训练权重都是
tf.Variable。 - 特征工程:对特征进行标准化、多项式扩展时利用广播减去均值。
- 自定义损失函数:需要对预测值和标签进行逐元素运算,广播机制保证不同形状的张量能够计算。
2. 核心理论精讲
2.1 张量:从标量到N维数组
张量是TensorFlow中所有数据的基本单位。从数学角度,张量是向量和矩阵向更高维度的推广:
- 标量(0维张量):单个数字,形状为
()。 - 向量(1维张量):一维数组,形状为
(n,)。 - 矩阵(2维张量):二维数组,形状为
(m, n)。 - 3维张量:形状为
(a, b, c),例如彩色图像批次:(batch, height, width)或加上通道:(batch, height, width, channels)。 - 更高维:深度学习中常见的4维、5维张量。
每个张量具备三个基本属性:
- 形状:一个整数元组,描述每个维度的大小。例如
(3, 4)表示3行4列。 - 数据类型:
tf.float32、tf.int32、tf.bool等。运算时通常要求数据类型一致。 - 设备:张量存储的位置,如
/CPU:0、/GPU:0。TensorFlow会自动管理设备放置。
2.2 tf.Tensor vs tf.Variable
| 特性 | tf.Tensor | tf.Variable |
|---|---|---|
| 可变性 | 不可变(immutable),每次运算产生新张量 | 可变,可原地修改内容 |
| 适用场景 | 输入数据、中间计算结果、常量 | 模型参数(权重、偏置) |
| 梯度追踪 | 默认不追踪(除非设置watch) | 默认自动被GradientTape追踪 |
| 创建方式 | tf.constant, tf.zeros, tf.ones, tf.random.* | tf.Variable(initial_value) |
| 更新方式 | 无法直接更新 | assign, assign_add, assign_sub |
重要:在深度学习中,模型参数必须使用tf.Variable,因为优化器需要通过梯度更新它们。
2.3 张量的数据类型
TensorFlow支持丰富的数值类型,常用列表:
| 数据类型 | 描述 | 典型用途 |
|---|---|---|
tf.float32 | 单精度浮点 | 神经网络默认精度,计算快 |
tf.float64 | 双精度浮点 | 高精度数值计算,但速度较慢 |
tf.int32 | 32位整数 | 索引、标签、序列长度 |
tf.int64 | 64位整数 | 大范围索引 |
tf.bool | 布尔型 | 掩码、条件判断 |
tf.string | 字符串 | 文本数据、文件名 |
类型转换使用tf.cast(tensor, dtype)。注意:不同类型之间不能直接运算(如float32 + int32会先隐式转换,但最佳实践是显式转换)。
2.4 维度变换:重塑数据布局
维度变换是数据预处理的核心操作,常用函数:
tf.reshape(tensor, shape):改变形状,但元素总数不变(-1表示自动推断)。tf.transpose(tensor, perm):交换维度(矩阵转置是二维特例)。tf.expand_dims(tensor, axis):在指定轴插入维度大小为1的新轴。tf.squeeze(tensor, axis):删除维度大小为1的轴(可指定轴)。tf.concat(values, axis):沿指定轴拼接多个张量。tf.stack(values, axis):沿新轴堆叠,增加维度。
原理:reshape不改变底层数据的一维顺序(行优先存储),只是改变视图。而transpose会重新排列数据。
2.5 广播机制:不同形状的运算规则
当两个张量进行逐元素运算(如+、-、*、/)时,如果形状不相等,TensorFlow会尝试广播较小的张量以匹配较大张量的形状。
广播规则(从尾部维度开始对齐):
- 从最后一个维度开始向前对齐。
- 如果两个维度相等,或其中一个为1,或其中一个缺失(视为1),则兼容。
- 如果不能兼容,抛出
InvalidArgumentError。
广播不会实际复制数据,而是通过有效的步长实现逻辑扩展,因此内存高效。
示例:
- 形状
(3,4)+(4,):(4,)广播为(3,4),合法。 - 形状
(3,4)+(3,1):(3,1)广播为(3,4),合法。 - 形状
(3,4)+(2,4):第一维3≠2且都不为1,非法。
手动广播:可通过tf.broadcast_to(tensor, shape)显式广播,但通常隐式广播就够了。
3. 环境搭建与工具配置
本课沿用第4课已经配置好的TensorFlow环境(推荐tf213)。无需额外安装。
启动Python交互环境或Jupyter Notebook:
conda activate tf213
python
或
jupyter notebook
确保导入:
import tensorflow as tf
print(tf.__version__) # 2.13.0
4. 代码实战教学
本节通过大量代码示例,逐行演示张量的创建、属性查看、形状变换和广播机制。建议读者在Jupyter中逐个单元格运行。
4.1 创建张量与基本属性
import tensorflow as tf
import numpy as np
# 1. 从Python标量/列表创建
scalar = tf.constant(3.14)
vector = tf.constant([1, 2, 3])
matrix = tf.constant([[1, 2], [3, 4]], dtype=tf.float32)
print("标量:", scalar)
print("向量形状:", vector.shape, " dtype:", vector.dtype)
print("矩阵:\n", matrix)
# 2. 全0、全1、单位矩阵
zeros = tf.zeros((2, 3))
ones = tf.ones((3, 2))
eye = tf.eye(4)
print("zeros:\n", zeros)
# 3. 与NumPy互操作
np_array = np.array([[1, 2], [3, 4]], dtype=np.float32)
tensor_from_np = tf.constant(np_array) # 或 tf.convert_to_tensor
print("from numpy:", tensor_from_np)
# 张量转numpy
back_to_np = tensor_from_np.numpy()
print("back to numpy:", back_to_np, type(back_to_np))
# 4. 随机张量
random_normal = tf.random.normal(shape=(3, 3), mean=0.0, stddev=1.0)
random_uniform = tf.random.uniform(shape=(2, 2), minval=0, maxval=10, dtype=tf.int32)
print("正态随机:\n", random_normal)
print("均匀随机整数:\n", random_uniform)
输出解读:
tf.constant创建的是不可变张量。- 形状属性
shape返回tf.TensorShape对象,可转为列表。 - 与NumPy互操作零拷贝(尽可能),但
.numpy()返回副本。
4.2 变量:可变的张量
# 创建变量
var_w = tf.Variable(initial_value=tf.random.normal((2, 3)), name='weight')
var_b = tf.Variable(tf.zeros(3), name='bias')
print("变量初值:\n", var_w.numpy())
print("偏置初值:", var_b.numpy())
# 变量更新
var_b.assign(tf.ones(3)) # 整体赋值
print("assign后:", var_b.numpy())
var_b.assign_add([1, 1, 1]) # 自增
print("assign_add后:", var_b.numpy())
# 部分更新(通过切片)
var_b[0].assign(100.0)
print("切片赋值后:", var_b.numpy())
# 变量转为张量(只读)
readonly = tf.identity(var_b) # 得到Tensor,不共享内存
var_b.assign_add([10,10,10])
print("原变量已变:", var_b.numpy())
print("只读张量未变:", readonly.numpy())
注意:变量可被tf.GradientTape自动监控,而张量需手动调用tape.watch()。
4.3 数据类型转换
# 不同dtype的创建
int_tensor = tf.constant([1, 2, 3], dtype=tf.int32)
float_tensor = tf.constant([1.0, 2.0, 3.0], dtype=tf.float32)
# 类型转换
int_to_float = tf.cast(int_tensor, tf.float32)
float_to_int = tf.cast(float_tensor, tf.int32)
print("int->float:", int_to_float.dtype)
print("float->int:", float_to_int.numpy()) # 截断小数
# 布尔类型
bool_tensor = tf.constant([True, False, True])
to_int = tf.cast(bool_tensor, tf.int32) # True->1, False->0
print("bool转int:", to_int.numpy())
# 类型不匹配会报错,需显式转换
# a = int_tensor + float_tensor # 报错
a = tf.cast(int_tensor, tf.float32) + float_tensor
print("统一dtype后运算:", a)
4.4 维度变换实战
4.4.1 reshape
tensor = tf.range(12) # 生成0~11,形状(12,)
print("原始:", tensor.numpy())
# reshape为3x4
reshaped = tf.reshape(tensor, (3, 4))
print("reshape(3,4):\n", reshaped.numpy())
# 使用-1自动推断
auto_reshape = tf.reshape(tensor, (2, -1)) # 2 x (12/2=6)
print("reshape(2,-1):\n", auto_reshape.numpy())
# 注意事项:-1只能出现一次
# tf.reshape(tensor, (-1, -1)) # 错误
# reshape不影响底层数据顺序(行优先)
# 验证:展平后顺序不变
flatten = tf.reshape(reshaped, (-1,))
print("flatten后与原顺序一致:", tf.reduce_all(tf.equal(tensor, flatten)).numpy())
4.4.2 transpose 维度交换
mat = tf.constant([[1, 2, 3],
[4, 5, 6]]) # 形状(2,3)
print("原始矩阵:\n", mat.numpy())
# 转置(二维)
transposed = tf.transpose(mat) # 形状(3,2)
print("转置后:\n", transposed.numpy())
# 更高维示例:四维图像 (batch, height, width, channels) -> (batch, width, height, channels)
img = tf.random.normal((4, 32, 32, 3))
print("原始形状:", img.shape) # (4,32,32,3)
# 交换高度和宽度维度,即轴1和轴2
img_swapped = tf.transpose(img, perm=[0, 2, 1, 3])
print("交换后形状:", img_swapped.shape) # (4,32,32,3) 但数据已重排
4.4.3 expand_dims 与 squeeze
vec = tf.constant([1, 2, 3]) # 形状(3,)
# 在axis=0处插入新轴 -> (1,3)
row_vec = tf.expand_dims(vec, axis=0)
print("row_vec形状:", row_vec.shape, "\n", row_vec.numpy())
# 在axis=1处插入新轴 -> (3,1)
col_vec = tf.expand_dims(vec, axis=1)
print("col_vec形状:", col_vec.shape, "\n", col_vec.numpy())
# squeeze: 移除大小为1的维度
tensor_with_ones = tf.ones((1, 3, 1, 2))
squeezed = tf.squeeze(tensor_with_ones) # 移除所有大小为1的维度 -> (3,2)
print("squeeze后:", squeezed.shape)
# 指定轴挤压
squeeze_axis0 = tf.squeeze(tensor_with_ones, axis=0) # (3,1,2)
print("指定axis=0后:", squeeze_axis0.shape)
4.4.4 concat 与 stack
a = tf.constant([[1, 2], [3, 4]])
b = tf.constant([[5, 6], [7, 8]])
# 沿行拼接(axis=0)
concat_row = tf.concat([a, b], axis=0)
print("concat axis=0:\n", concat_row.numpy()) # 4x2
# 沿列拼接(axis=1)
concat_col = tf.concat([a, b], axis=1)
print("concat axis=1:\n", concat_col.numpy()) # 2x4
# stack: 新增加维度
stacked = tf.stack([a, b], axis=0) # 形状(2,2,2)
print("stack shape:", stacked.shape)
4.5 广播机制详解
# 示例1: 向量+标量
vec = tf.constant([1, 2, 3])
scalar = tf.constant(2)
result = vec + scalar # 标量广播到(3,)
print("向量+标量:", result.numpy()) # [3,4,5]
# 示例2: 矩阵+向量(行向量)
mat = tf.constant([[1, 2, 3],
[4, 5, 6]]) # (2,3)
row_vec = tf.constant([10, 20, 30]) # (3,)
result = mat + row_vec # (3,)广播到(2,3)
print("矩阵+行向量:\n", result.numpy())
# 结果: [[11,22,33], [14,25,36]]
# 示例3: 矩阵+列向量
col_vec = tf.constant([[10], [20]]) # (2,1)
result = mat + col_vec
print("矩阵+列向量:\n", result.numpy())
# 结果: [[11,12,13], [24,25,26]]
# 示例4: 高阶广播
x = tf.ones((2, 3, 4)) # 形状(2,3,4)
y = tf.ones((3, 1)) # 形状(3,1)
# 广播过程: y形状补全为(1,3,1) -> 扩展为(2,3,4)
z = x + y
print("(2,3,4)+(3,1) 广播后形状:", z.shape) # (2,3,4)
# 示例5: 非法广播,会报错
try:
invalid = tf.ones((2,3)) + tf.ones((3,2))
except tf.errors.InvalidArgumentError as e:
print("非法广播报错:", e)
手动广播:使用tf.broadcast_to
small = tf.constant([1, 2, 3])
target_shape = (4, 3)
broadcasted = tf.broadcast_to(small, target_shape)
print("手动广播:\n", broadcasted.numpy())
# 输出: 每行都是[1,2,3]
5. 案例实操演练
案例:对一批图像数据进行标准化预处理,并调整维度以适应卷积神经网络输入
5.1 任务描述
假设我们有一个NumPy数组表示的图像批次,形状为(32, 64, 64, 3),数据类型uint8(像素值0~255)。需求:
- 转换为
float32。 - 减去每个通道的均值(均值预先计算为
[0.485, 0.456, 0.406]),并除以标准差[0.229, 0.224, 0.225](ImageNet标准)。 - 调整形状为
(32, 64, 64, 3)不变(但顺序可能需要从HWC转为CHW?根据模型而定)。 - 最终张量形状要求模型输入为
(batch, 64, 64, 3),但我们可以展示如何灵活变换。
5.2 代码实现
import tensorflow as tf
import numpy as np
# 模拟图像批次
batch_size = 32
height, width, channels = 64, 64, 3
images = np.random.randint(0, 256, size=(batch_size, height, width, channels), dtype=np.uint8)
# 转换为张量并转为float32
tensor_images = tf.constant(images, dtype=tf.float32)
print("原始形状:", tensor_images.shape, " dtype:", tensor_images.dtype)
# 定义均值和标准差(每个通道)
mean = tf.constant([0.485, 0.456, 0.406], dtype=tf.float32) # 形状(3,)
std = tf.constant([0.229, 0.224, 0.225], dtype=tf.float32)
# 利用广播机制减去均值并除以标准差
# 注意: tensor_images形状(32,64,64,3), mean形状(3,)
# 广播时,mean会扩展为(1,1,1,3),再扩展为(32,64,64,3)
normalized = (tensor_images / 255.0 - mean) / std
print("标准化后形状:", normalized.shape)
print("第一个像素点标准化值:", normalized[0,0,0].numpy())
# 如果需要转换为NHWC(默认就是) 到 NCHW (batch, channels, height, width)
if False: # 演示转换
nchw = tf.transpose(normalized, perm=[0, 3, 1, 2])
print("NCHW形状:", nchw.shape) # (32,3,64,64)
5.3 扩展:动态调整批次维度
假设我们需要将单张图像(H,W,3)输入模型,但模型期望批次维度,可以使用expand_dims:
single_image = tf.random.normal((64, 64, 3))
batch_image = tf.expand_dims(single_image, axis=0) # (1,64,64,3)
print("增加批次维度后:", batch_image.shape)
5.4 可视化广播效果
import matplotlib.pyplot as plt
# 显示原图与标准化后第一个通道的均值
sample_img = images[0] / 255.0
plt.figure(figsize=(6,3))
plt.subplot(1,2,1)
plt.imshow(sample_img)
plt.title("Original")
plt.subplot(1,2,2)
plt.imshow(normalized[0].numpy()[:,:,0], cmap='gray')
plt.title("Normalized (channel 0)")
plt.show()
6. 常见坑点与排错总结
6.1 形状操作坑点
- 坑1:
tf.reshape中-1推断出错,例如元素总数不能整除时。- 解决:确保张量总元素数可被
-1之外的其他维度乘积整除。
- 解决:确保张量总元素数可被
- 坑2:混淆
reshape与transpose。reshape不改变数据的一维顺序,而transpose会重新排列。- 示例:
[[1,2],[3,4]]reshape成(1,4)得[1,2,3,4],而transpose后reshape可能得到[1,3,2,4]。
- 示例:
- 坑3:对
tf.Variable使用reshape后得到的是Tensor,无法原地修改变量形状。- 解决:变量形状一般固定,若要改变需重新
assign新形状张量(不推荐)。
- 解决:变量形状一般固定,若要改变需重新
6.2 广播误区
- 坑4:误认为广播会复制数据导致内存爆炸,但实际上只是逻辑扩展。
- 说明:广播非常高效,放心使用。
- 坑5:广播规则理解错误,例如形状
(2,3)与(2,1)可以广播,但(2,3)与(2,)也可以(尾部对齐)。常见错误以为只有完全匹配才能运算。 - 坑6:在
tf.losses中传递形状不匹配的预测值和标签时,广播可能导致意外扩大维度(例如(16,)与(16,1)广播后变成(16,16))。- 解决:显式
reshape或squeeze确保两者形状完全一致。
- 解决:显式
6.3 数据类型与设备
- 坑7:混合精度导致梯度为
NaN。例如在float16和float32之间未正确转换。- 解决:统一使用
tf.float32,除非明确使用混合精度策略。
- 解决:统一使用
- 坑8:使用
tf.constant创建大张量导致内存不足。- 解决:使用
tf.Variable配合assign逐步填充,或使用tf.zeros、tf.ones惰性初始化。
- 解决:使用
- 坑9:张量默认存储在CPU上,但计算可能在GPU,导致跨设备复制开销。
- 解决:使用
tf.device上下文指定设备,或者依赖自动设备放置(通常合理)。
- 解决:使用
6.4 变量的常见错误
- 坑10:在
tf.function内修改变量值但没有使用.assign,而是直接用=,导致创建新局部变量。- 解决:始终使用
assign、assign_add等方法。
- 解决:始终使用
- 坑11:重复调用
tf.Variable创建相同变量,导致参数数量增加。- 解决:在模型
__init__中定义变量,在call中复用。
- 解决:在模型
7. 知识点总结 + 课后作业
7.1 核心知识点梳理
- 张量属性:
shape(形状)、dtype(数据类型)、device(设备位置)。 - 创建方式:
tf.constant(不可变)、tf.Variable(可变)、tf.zeros/ones/eye、tf.random.*。 - 数据类型转换:
tf.cast。 - 形状变换:
reshape:改变视图,不改变底层数据顺序。transpose:交换维度,重新排列数据。expand_dims/squeeze:增加/删除大小为1的维度。concat/stack:合并张量。
- 广播机制:从尾部对齐,维度相等或1则兼容;高效逻辑扩展。
- 变量与张量区别:变量可更新、可被优化器追踪;张量不可变。
7.2 基础作业
- 创建一个形状为
(4, 4)的随机整数张量(范围0~10),输出其形状、数据类型,并转换为tf.float32。 - 将上述张量重塑为
(2, 8),再转置为(8, 2),观察元素顺序变化。 - 创建一个形状为
(3, 3)的全1张量,创建一个形状为(3, 1)的全2张量,两者相加,写出结果。
7.3 进阶实操作业
任务:实现一个张量工具函数,要求:
- 函数
prepare_batch(images, labels, target_shape)接收:images: 形状为(batch, h, w, c)的uint8张量。labels: 形状为(batch,)的整数标签。target_shape: 元组(new_h, new_w),需要将图像缩放到该尺寸(使用tf.image.resize,双线性插值)。
- 函数内部完成:
- 将图像转换为
float32,缩放到target_shape。 - 将像素值归一化到[0,1]。
- 对每个通道减去均值
[0.5, 0.5, 0.5](广播)。 - 返回处理后的图像张量和原始标签。
- 将图像转换为
- 使用
tf.random.normal模拟一个批次测试该函数,并输出最终形状。
7.4 思考拓展题
- 假设有张量
A形状(2, 3, 4, 5),张量B形状(4, 1)。判断它们能否直接相加?若能,写出广播后的形状。 - 在深度学习中,为什么模型权重通常使用
tf.Variable而输入使用tf.Tensor?如果误将输入作为tf.Variable会有什么后果? - 研究
tf.einsum函数,用它来实现矩阵乘法和转置,并说明其与tf.transpose和tf.matmul的关系。
下一课预告:张量运算全解——我们将深入探讨数学运算、矩阵运算、逻辑运算以及高级索引切片,掌握神经网络前向传播中的所有计算细节,为构建复杂模型打下坚实的运算基础。
🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航
第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

320

被折叠的 条评论
为什么被折叠?



