3. PyTorch核心张量操作:张量的创建、数据类型、形状变换、索引切片、数学运算、广播机制以及与NumPy的互转
张量(Tensor)是PyTorch中最核心的数据结构,它既是深度学习模型参数与中间特征图的载体,也是所有自动微分、反向传播和GPU并行计算的基础。如果把PyTorch比作一座工厂,张量就是工厂中流动的“原材料”和“半成品”。本章将系统梳理张量的创建方式、数据类型体系、形状变换、索引切片、数学运算、广播机制以及与NumPy的互转方法,帮助学习者建立对PyTorch底层数据流的完整认知。这些操作在后续的模型搭建、数据预处理、损失计算和推理部署中会被反复使用,掌握其细节将直接决定代码的简洁性、可读性和运行效率。
上图从六个维度勾勒了PyTorch张量操作的全景:创建方式决定了数据的来源与初始状态;数据类型影响精度与内存占用;形状变换是维度适配的核心手段;索引切片用于精准提取子区域;数学运算完成特征变换与损失计算;广播机制则让不同形状的张量能够高效参与运算。与NumPy的互转则是连接传统科学计算与深度学习生态的桥梁。下面逐一深入展开。
3.1 张量的创建:从数据源到张量对象
核心原理
PyTorch张量的创建本质上是将Python原生数据、NumPy数组或随机数生成器产生的数据封装为具有shape、dtype、device和requires_grad属性的张量对象。创建方式决定了张量的初始值分布、内存布局以及是否参与自动微分。常用的创建API可分为四类:直接构造(torch.tensor)、全值填充(zeros/ones/eye/full)、随机采样(rand/randn/randint)以及序列生成(arange/linspace)。
操作流程与代码示例
import torch
import numpy as np
# 1. 直接构造
a = torch.tensor([[1, 2], [3, 4]]) # 从嵌套列表
b = torch.tensor(np.array([1.0, 2.0])) # 从NumPy数组
# 2. 全值填充
c = torch.zeros(2, 3) # 2x3全零
d = torch.ones(2, 2, dtype=torch.float64) # 指定dtype
e = torch.eye(3) # 3x3单位矩阵
f = torch.full((2, 2), 7) # 全7
# 3. 随机采样
g = torch.rand(3, 3) # 均匀分布[0,1)
h = torch.randn(3, 3) # 标准正态分布
i = torch.randint(0, 10, (2, 3)) # 整数随机
# 4. 序列生成
j = torch.arange(0, 10, step=2) # [0,2,4,6,8]
k = torch.linspace(0, 1, steps=5) # 等间隔5个点
案例拆解:初始化模型权重
在搭建神经网络时,权重初始化直接影响训练收敛速度。例如,线性层权重通常使用xavier_uniform_或kaiming_normal_初始化,而偏置常初始化为零。以下代码展示了如何创建并初始化一个3×4的权重矩阵:
w = torch.empty(3, 4) # 未初始化内存
torch.nn.init.xavier_uniform_(w) # Xavier均匀初始化
b = torch.zeros(4, dtype=torch.float32) # 偏置置零
这里使用torch.empty而非torch.zeros,是因为初始化函数会覆盖全部元素,先分配内存再填充可以避免一次额外的写操作,提升效率。
dtype。默认的torch.float32适合大多数场景,但涉及整数索引、布尔掩码或高精度计算时,应显式指定torch.int64、torch.bool或torch.float64,避免隐式转换带来的精度损失或类型错误。torch.tensor()会复制数据,而torch.as_tensor()在输入为NumPy数组且dtype一致时共享内存。若需避免意外修改原始数据,请使用torch.tensor();若追求性能且明确不修改原数组,可使用torch.as_tensor()。3.2 数据类型与设备管理
核心原理
PyTorch支持丰富的数据类型,包括浮点型(float16/32/64)、整型(int8/16/32/64)、布尔型(bool)以及复数型(complex64/128)。数据类型决定了每个元素占用的内存字节数、数值表示范围以及计算精度。深度学习训练默认使用float32,混合精度训练则使用float16配合float32主权重。设备管理(CPU/GPU)则决定了张量存储的物理位置和计算执行单元。
类型转换与设备迁移
# 类型转换
x = torch.randn(3, 3)
x_half = x.half() # 转float16
x_double = x.double() # 转float64
x_int = x.long() # 转int64(截断小数)
# 设备迁移
if torch.cuda.is_available():
x_gpu = x.cuda() # CPU→GPU
x_cpu = x_gpu.cpu() # GPU→CPU
# 推荐写法:使用to()统一管理
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
x_dev = x.to(device)
知识结构表:常用数据类型对比
| 数据类型 | dtype | 字节数 | 典型用途 | 注意事项 |
|---|---|---|---|---|
| float32 | torch.float32 | 4 | 默认训练精度 | 精度适中,速度较快 |
| float64 | torch.float64 | 8 | 数值计算、科学仿真 | 内存翻倍,速度较慢 |
| float16 | torch.float16 | 2 | 混合精度训练 | 需配合梯度缩放 |
| int64 | torch.int64 | 8 | 索引、标签 | PyTorch默认整数类型 |
| bool | torch.bool | 1 | 掩码、条件判断 | 不能直接参与算术运算 |
3.3 形状变换:维度操作的基石
核心原理
形状变换是深度学习中最频繁的操作之一。其本质是重新解释张量的内存布局,或通过复制数据生成新的视图。view()和reshape()用于改变形状,transpose()和permute()用于交换维度顺序,squeeze()和unsqueeze()用于压缩或扩展维度,expand()和repeat()用于扩展维度大小。理解“视图(view)”与“拷贝(copy)”的区别是避免内存爆炸和意外修改的关键。
操作流程与代码示例
x = torch.randn(2, 3, 4) # 形状[2,3,4]
# view/reshape:改变形状(view要求内存连续)
y1 = x.view(2, 12) # [2,12]
y2 = x.reshape(6, 4) # [6,4],自动处理非连续
# transpose/permute:交换维度
z1 = x.transpose(0, 1) # [3,2,4]
z2 = x.permute(2, 0, 1) # [4,2,3]
# squeeze/unsqueeze:压缩/扩展维度
a = torch.randn(1, 3, 1, 4)
b = a.squeeze() # [3,4],去掉所有1维
c = a.unsqueeze(0) # [1,1,3,1,4],增加第0维
# expand/repeat:扩展维度
d = torch.randn(1, 3)
e = d.expand(4, 3) # [4,3],视图,不复制数据
f = d.repeat(4, 1) # [4,3],拷贝数据
案例拆解:图像数据维度调整
在计算机视觉任务中,图像张量通常以[B, C, H, W]存储(Batch、Channel、Height、Width)。当需要将单张图像送入模型时,需要增加Batch维度;当需要可视化时,又需要将Channel维度移到最后。以下代码展示了完整的维度变换流程:
img = torch.randn(3, 224, 224) # [C,H,W]
img_batch = img.unsqueeze(0) # [1,3,224,224]
img_hwc = img_batch.permute(0, 2, 3, 1) # [1,224,224,3]
img_flat = img_batch.view(1, -1) # [1, 3*224*224]
view()要求张量在内存中连续存储。经过transpose()或permute()后的张量通常不连续,此时使用view()会报错,应改用reshape()或先调用contiguous()。此外,expand()返回的是视图,修改扩展后的张量会影响原始张量,而repeat()返回的是独立拷贝。3.4 索引切片:精准提取数据子集
核心原理
PyTorch的索引切片语法与NumPy高度一致,支持基本索引(整数、切片)、布尔索引(掩码)和花式索引(整数数组)。基本索引返回视图(共享内存),而布尔索引和花式索引通常返回拷贝。理解这一区别对于内存管理和避免意外修改至关重要。
操作流程与代码示例
x = torch.arange(12).reshape(3, 4)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 基本索引
a = x[1, 2] # 标量6
b = x[0:2, 1:3] # [[1,2],[5,6]]
c = x[:, -1] # 最后一列 [3,7,11]
# 布尔索引
mask = x > 5
d = x[mask] # 一维张量 [6,7,8,9,10,11]
# 花式索引
e = x[[0, 2], [1, 3]] # [1, 11]
f = x[[0, 2], :] # 第0行和第2行
案例拆解:数据批处理中的索引应用
在训练循环中,经常需要从数据集中随机抽取一个批次。以下代码展示了如何使用花式索引实现mini-batch采样:
dataset = torch.randn(100, 3, 32, 32) # 100张32x32的RGB图像
indices = torch.randint(0, 100, (16,)) # 随机16个索引
batch = dataset[indices] # [16,3,32,32]
# 同时提取标签和特征
labels = torch.randint(0, 10, (100,))
batch_labels = labels[indices] # 对应标签
x[0:2, 0:2] = 0会将左上角2×2区域置零。若需要独立副本,请显式调用.clone()。3.5 数学运算与广播机制
核心原理
PyTorch的数学运算分为逐元素运算(+ - * /、torch.sqrt、torch.exp)、矩阵运算(@、torch.matmul)、归约运算(sum、mean、max)以及就地运算(add_、mul_)。广播机制允许不同形状的张量进行运算,其规则是:从尾部维度对齐,每个维度要么相等,要么其中一个为1,要么缺失(视为1)。
广播机制详解
# 广播示例
a = torch.randn(3, 1) # [3,1]
b = torch.randn(1, 4) # [1,4]
c = a + b # [3,4],自动扩展
# 等价于
a_expanded = a.expand(3, 4)
b_expanded = b.expand(3, 4)
c_manual = a_expanded + b_expanded
# 不匹配示例(会报错)
d = torch.randn(3, 4)
e = torch.randn(2, 4)
# f = d + e # RuntimeError: The size of tensor a (3) must match...
案例拆解:特征归一化中的广播应用
在批归一化(Batch Normalization)中,需要对每个通道计算均值和方差,然后对整批数据进行标准化。广播机制使得这一操作变得简洁高效:
batch = torch.randn(32, 3, 64, 64) # [B,C,H,W]
mean = batch.mean(dim=(0, 2, 3), keepdim=True) # [1,3,1,1]
std = batch.std(dim=(0, 2, 3), keepdim=True) # [1,3,1,1]
normalized = (batch - mean) / std # 广播自动完成
这里mean和std的形状为[1,3,1,1],与[32,3,64,64]的batch进行运算时,广播机制自动将mean扩展到[32,3,64,64],避免了显式循环或复制操作。
expand操作,大幅提升代码简洁性和运行速度。但需注意,广播会隐式创建临时张量,在超大张量运算时可能增加内存峰值,此时应权衡是否使用torch.matmul或einsum替代。3.6 与NumPy的互转:生态桥接
核心原理
PyTorch与NumPy共享底层内存布局(在CPU上),因此两者之间的转换代价极低。torch.from_numpy()将NumPy数组转为张量,tensor.numpy()将张量转为NumPy数组。关键特性是:当两者共享内存时,修改一方会影响另一方。GPU上的张量必须先迁移到CPU才能转为NumPy数组。
操作流程与代码示例
import numpy as np
# NumPy → PyTorch
np_array = np.random.randn(3, 4)
tensor_from_np = torch.from_numpy(np_array) # 共享内存
tensor_copy = torch.tensor(np_array) # 独立拷贝
# PyTorch → NumPy
tensor = torch.randn(3, 4)
np_from_tensor = tensor.numpy() # 共享内存
np_copy = tensor.numpy().copy() # 独立拷贝
# GPU张量转换
if torch.cuda.is_available():
tensor_gpu = tensor.cuda()
np_from_gpu = tensor_gpu.cpu().numpy() # 先迁移到CPU
案例拆解:数据预处理流水线
在实际项目中,数据通常以NumPy格式存储(如图像数组、CSV读取结果),需要转换为PyTorch张量送入模型。以下是一个完整的预处理流程:
# 假设从磁盘加载图像为NumPy数组
image_np = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
# 转换为float32并归一化
image_tensor = torch.from_numpy(image_np).float() # [224,224,3]
image_tensor = image_tensor.permute(2, 0, 1) # [3,224,224]
image_tensor = image_tensor / 255.0 # 归一化到[0,1]
# 增加Batch维度
image_batch = image_tensor.unsqueeze(0) # [1,3,224,224]
torch.from_numpy()会报错。例如,np.float64数组不能直接转为torch.float32张量,需先调用.astype(np.float32)。此外,共享内存的张量在反向传播时若被修改,可能导致梯度计算错误,建议在需要梯度时使用.clone()。3.7 综合实战:张量操作在模型训练中的应用
场景描述
假设我们要训练一个简单的线性回归模型,输入特征维度为10,输出维度为1。整个训练循环中会用到本章几乎所有张量操作:创建权重、数据批次索引、形状变换、数学运算、广播以及NumPy互转。
完整代码与拆解
import torch
import numpy as np
# 1. 生成模拟数据(NumPy → 张量)
np.random.seed(42)
X_np = np.random.randn(1000, 10).astype(np.float32)
y_np = (X_np @ np.random.randn(10, 1).astype(np.float32) + 0.5).squeeze()
X = torch.from_numpy(X_np) # [1000,10]
y = torch.from_numpy(y_np).float() # [1000]
# 2. 初始化模型参数
w = torch.randn(10, 1, requires_grad=True) # [10,1]
b = torch.zeros(1, requires_grad=True) # [1]
# 3. 训练循环
learning_rate = 0.01
for epoch in range(100):
# 随机批次索引(花式索引)
indices = torch.randint(0, 1000, (64,))
X_batch = X[indices] # [64,10]
y_batch = y[indices] # [64]
# 前向传播(矩阵乘法 + 广播)
y_pred = X_batch @ w + b # [64,1]
y_pred = y_pred.squeeze() # [64]
# 损失计算(逐元素运算 + 归约)
loss = ((y_pred - y_batch) ** 2).mean()
# 反向传播
loss.backward()
# 参数更新(就地运算)
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad
w.grad.zero_()
b.grad.zero_()
if epoch % 20 == 0:
print(f'Epoch {epoch}, Loss: {loss.item():.4f}')
# 4. 结果转换回NumPy
w_np = w.detach().numpy()
print(f'权重: {w_np.flatten()[:3]}...')
复盘与经验
这个案例覆盖了张量创建(torch.randn、torch.zeros)、NumPy互转(from_numpy、numpy)、花式索引(X[indices])、矩阵运算(@)、广播(X_batch @ w + b)、归约(mean)、就地更新(-=)以及梯度清零(zero_)。关键点在于:requires_grad=True的张量在更新时必须包裹在torch.no_grad()中,否则会构建计算图导致内存泄漏;detach()用于将张量从计算图中分离,以便转为NumPy。
torch.utils.data.DataLoader替代手动