3. PyTorch核心张量操作:张量的创建、数据类型、形状变换、索引切片、数学运算、广播机制以及与NumPy的互转

张量(Tensor)是PyTorch中最核心的数据结构,它既是深度学习模型参数与中间特征图的载体,也是所有自动微分、反向传播和GPU并行计算的基础。如果把PyTorch比作一座工厂,张量就是工厂中流动的“原材料”和“半成品”。本章将系统梳理张量的创建方式、数据类型体系、形状变换、索引切片、数学运算、广播机制以及与NumPy的互转方法,帮助学习者建立对PyTorch底层数据流的完整认知。这些操作在后续的模型搭建、数据预处理、损失计算和推理部署中会被反复使用,掌握其细节将直接决定代码的简洁性、可读性和运行效率。

PyTorch张量操作全景图 创建方式 • torch.tensor(list) • torch.zeros/ones/eye • torch.rand/randn • torch.arange/linspace • torch.full/empty 数据类型 • float32/float64 • int64/int32/int16 • bool • 转换: .to()/.float() • 默认: float32 形状变换 • reshape/view • transpose/permute • squeeze/unsqueeze • expand/repeat • flatten 索引切片 • t[i, j] 基本索引 • t[1:3, :] 切片 • t[mask] 布尔索引 • t[[0,2]] 花式索引 • 视图与拷贝 数学运算 • 逐元素: + - * / • 矩阵: @ / matmul • 归约: sum/mean/max • 激活: relu/sigmoid • 就地: add_/mul_ 广播机制 • 从尾部对齐维度 • 维度为1可扩展 • 缺失维度补1 • 例: [3,1]+[1,4]→[3,4] • 不匹配则报错 NumPy互转 • torch.from_numpy() • tensor.numpy() • 共享内存(CPU) • GPU需先.cpu() • 注意dtype一致性 核心要点 • 默认float32 • 视图节省内存 • 广播避免循环 • 设备一致性 • 梯度自动追踪 六大核心操作模块 · 相互配合构成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字节数典型用途注意事项
float32torch.float324默认训练精度精度适中,速度较快
float64torch.float648数值计算、科学仿真内存翻倍,速度较慢
float16torch.float162混合精度训练需配合梯度缩放
int64torch.int648索引、标签PyTorch默认整数类型
booltorch.bool1掩码、条件判断不能直接参与算术运算
总结复盘:类型转换的核心原则是“显式优于隐式”。在模型输入阶段统一dtype,在损失计算时保持float32,在保存模型时使用float32权重,避免因类型不一致导致的梯度错误或推理精度下降。

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],避免了显式循环或复制操作。

总结复盘:广播机制是PyTorch性能优化的核心武器。合理利用广播可以避免显式循环和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]
风险提示:当NumPy数组的dtype与目标张量不一致时,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替代手动