3、PyTorch基础:张量(Tensor)的创建、属性、数据类型、设备管理(CPU/GPU)

张量(Tensor)是PyTorch中最核心的数据结构,可以将其理解为多维数组的泛化形式——标量是0维张量,向量是1维张量,矩阵是2维张量,而更高维度的数据(如图像批次、视频序列、Transformer中的注意力矩阵)则对应3维及以上的张量。与NumPy的ndarray相比,PyTorch张量的关键差异在于:它能够自动记录计算图以支持反向传播,并且可以无缝地在CPU与GPU(CUDA)之间迁移,从而充分利用硬件加速能力。本章将系统讲解张量的创建方法、核心属性、数据类型体系以及设备管理策略,帮助读者建立扎实的张量操作基础。

PyTorch张量核心知识框架 张量 Tensor 创建方式 torch.tensor / torch.Tensor torch.zeros / ones / eye torch.arange / linspace torch.rand / randn / randint 核心属性 shape / ndim / numel() dtype / device / layout requires_grad / grad_fn stride / storage_offset 数据类型 float32 / float64 / float16 int64 / int32 / uint8 bool / bfloat16 torch.set_default_dtype() 设备管理 .to(device) / .cuda() torch.device('cuda:0') CPU ↔ GPU 数据迁移 多GPU并行 (DataParallel) 图:张量学习四维框架——创建、属性、数据类型、设备管理

3.1 张量的创建:从基础构造到高级初始化

核心原理

PyTorch提供了多种张量创建API,按使用场景可分为三类:直接构造(从Python数据或NumPy数组转换)、占位初始化(按形状生成全0、全1或单位矩阵)、随机采样(从特定概率分布中生成数据)。直接构造适合小规模测试数据,占位初始化常用于模型权重或掩码的预分配,随机采样则用于初始化神经网络参数或模拟噪声。理解这些API的底层差异(如是否参与梯度计算、是否共享内存)是高效使用PyTorch的前提。

操作流程与代码示例

import torch
import numpy as np

# 1. 直接构造
data = [[1, 2], [3, 4]]
t1 = torch.tensor(data)                    # 从Python列表创建
t2 = torch.from_numpy(np.array(data))      # 从NumPy创建(共享内存)
t3 = torch.Tensor(2, 3)                    # 未初始化(使用当前默认dtype)

# 2. 占位初始化
zeros = torch.zeros(2, 3)                  # 全0
ones = torch.ones(2, 3)                    # 全1
eye = torch.eye(3)                         # 3x3单位矩阵
full = torch.full((2, 2), 7)               # 填充指定值

# 3. 随机采样
rand_t = torch.rand(3, 3)                  # 均匀分布 [0, 1)
randn_t = torch.randn(3, 3)                # 标准正态分布
randint_t = torch.randint(0, 10, (3, 3))   # 整数均匀分布
arange_t = torch.arange(0, 10, 2)          # 等差序列 [0, 2, 4, 6, 8]
linspace_t = torch.linspace(0, 1, steps=5) # 等间隔序列

案例拆解:初始化一个图像分类模型的输入张量

假设我们要训练一个CIFAR-10图像分类模型,输入为32×32像素的RGB图像,批次大小为64。我们需要创建一个形状为(64, 3, 32, 32)的张量,其中64为批次维度,3为通道数(R/G/B),32×32为空间尺寸。实际场景中,数据通常从文件加载并归一化,但为了演示张量创建,我们模拟随机数据:

batch_size, channels, height, width = 64, 3, 32, 32
# 使用randn模拟标准化的图像数据(均值0,方差1)
image_batch = torch.randn(batch_size, channels, height, width)
print(f"输入张量形状: {image_batch.shape}")  # torch.Size([64, 3, 32, 32])
print(f"元素总数: {image_batch.numel()}")    # 196608

这里选择randn而非rand,是因为标准化后的图像数据更接近正态分布。若使用rand生成[0,1)均匀分布,则需额外进行归一化处理。该案例展示了如何根据模型需求确定张量形状,并选择合适的随机分布。

常见误区与总结

风险提示:使用torch.Tensor(2, 3)创建的是未初始化的张量,其值取决于内存中的残留数据,可能包含NaN或极大值。在需要确定性初始化的场景(如模型权重)中,务必使用torch.zeros、torch.ones或torch.randn等显式初始化方法。

另一个常见误区是混淆torch.tensor()与torch.Tensor():前者是函数,会根据输入数据推断dtype;后者是类构造函数,默认使用torch.float32且不复制数据(若传入已有张量)。此外,torch.from_numpy()创建的张量与NumPy数组共享底层内存,修改一方会影响另一方,这在需要避免数据复制时非常有用,但也容易引发意外副作用。

创建方法适用场景注意事项
torch.tensor(data)小规模测试数据自动推断dtype,复制数据
torch.from_numpy(np_arr)与NumPy互操作共享内存,修改互相影响
torch.zeros/ones/eye掩码、单位矩阵默认float32,可指定dtype
torch.rand/randn权重初始化、噪声rand为均匀分布,randn为正态分布
torch.arange/linspace生成序列数据注意endpoint参数(linspace默认包含终点)

3.2 张量的核心属性:形状、维度与内存布局

核心原理

张量的属性描述了其数据结构与内存组织方式。shape(或size())返回各维度大小,ndim表示维度数量,numel()计算元素总数。更底层地,stride定义了沿每个维度移动一个元素所需的步长(以元素个数计),storage_offset表示第一个元素在底层存储中的偏移量。这些属性决定了张量视图(view)操作的内存效率——视图共享底层数据,仅改变stride和offset,因此几乎零开销。

属性查看与操作

t = torch.randn(4, 6)
print(f"形状: {t.shape}")          # torch.Size([4, 6])
print(f"维度: {t.ndim}")           # 2
print(f"元素数: {t.numel()}")      # 24
print(f"步长: {t.stride()}")       # (6, 1) 表示第0维步长6,第1维步长1
print(f"存储偏移: {t.storage_offset()}")  # 0

# 视图操作(共享内存)
t_view = t.view(3, 8)              # 改变形状,但共享底层数据
t_reshape = t.reshape(3, 8)        # 可能复制数据(若无法共享内存)
t_transposed = t.t()               # 转置,stride变为(1, 6)

案例拆解:理解连续内存与非连续张量

在深度学习模型中,经常需要对张量进行转置或切片操作。例如,在Transformer中,注意力矩阵的形状为(batch, heads, seq_len, seq_len),转置操作transpose(1, 2)会生成非连续张量。此时若直接调用view()会报错,必须先调用contiguous():

attn = torch.randn(2, 4, 8, 8)   # (batch=2, heads=4, seq=8, seq=8)
attn_t = attn.transpose(1, 2)     # 变为 (2, 8, 4, 8),非连续
print(f"转置后stride: {attn_t.stride()}")  # (256, 8, 64, 1)

# 以下操作会报错:RuntimeError: view size is not compatible
# attn_t.view(2, 8, 32)
# 正确做法:先contiguous()再view
attn_contig = attn_t.contiguous().view(2, 8, 32)
print(f"连续化后形状: {attn_contig.shape}")

该案例揭示了view()与reshape()的本质区别:view()要求张量在内存中连续,而reshape()在非连续时会自动复制数据。理解stride有助于预测哪些操作会触发内存复制,从而优化性能。

常见误区与总结

经验提示:在编写自定义层或损失函数时,优先使用reshape()而非view(),因为前者更安全(自动处理非连续情况)。但若确定张量连续且追求极致性能,view()零拷贝的特性更优。

另一个易混淆点是shape与size():两者等价,但shape是属性,size()是方法。此外,numel()返回的是Python整数,而shape返回的是torch.Size对象(本质上是tuple的子类),支持索引访问。

3.3 数据类型(dtype):精度与性能的权衡

核心原理

PyTorch支持多种数据类型,从8位整数到64位浮点数,以及专为深度学习设计的半精度float16和bfloat16。默认的torch.float32(单精度)在精度和性能间取得平衡,是大多数模型的默认选择。float64(双精度)用于需要高数值稳定性的场景(如某些科学计算),但内存占用翻倍且GPU计算速度显著下降。float16和bfloat16则用于混合精度训练,可大幅减少显存占用并加速计算,但需注意数值溢出风险。

数据类型转换与检查

t = torch.tensor([1.5, 2.5])
print(f"默认dtype: {t.dtype}")  # torch.float32

# 显式指定dtype
t_f64 = t.double()              # 转为float64
t_f16 = t.half()                # 转为float16
t_i64 = t.long()                # 转为int64
t_bool = t.bool()               # 转为bool(非零为True)

# 创建时指定
t_custom = torch.zeros(3, dtype=torch.float64)
print(f"自定义dtype: {t_custom.dtype}")

# 查看默认dtype并修改
print(f"全局默认dtype: {torch.get_default_dtype()}")  # torch.float32
torch.set_default_dtype(torch.float64)
print(f"修改后默认dtype: {torch.get_default_dtype()}")

案例拆解:混合精度训练中的dtype管理

在训练大型语言模型时,显存往往成为瓶颈。以(1.5B参数)为例,若使用float32存储,仅权重就需要6GB显存(1.5B×4字节)。改用float16后,显存占用减半至3GB。但直接使用float16训练会遇到梯度下溢问题——当梯度值小于2^-24时会被舍入为0。解决方案是采用混合精度训练:权重和激活值使用float16,优化器状态和主权重保留float32,并通过损失缩放(loss scaling)避免梯度下溢。

# 模拟混合精度训练的关键步骤
model = torch.nn.Linear(1024, 1024).half()  # 模型转为半精度
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

for step in range(10):
    inputs = torch.randn(64, 1024).half()   # 输入也转为半精度
    outputs = model(inputs)
    loss = outputs.sum()                    # 简化损失计算
    
    optimizer.zero_grad()
    loss.backward()
    # 梯度裁剪前需转为float32,避免溢出
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    optimizer.step()

实际工程中,PyTorch提供了torch.cuda.amp模块自动处理精度转换和损失缩放,但理解底层dtype机制有助于调试精度相关的问题(如损失不下降、梯度为NaN)。

常见误区与总结

风险提示:不要随意将模型权重转为float64,这会导致GPU显存占用翻倍且计算速度下降数倍。除非进行数值稳定性分析,否则保持默认的float32即可。另外,float16的表示范围约为±65504,当中间结果超过此范围时会变为inf,需配合损失缩放使用。
dtype位数范围/精度典型应用
float3232约1e-38 ~ 3e38,7位小数默认训练/推理
float6464约1e-308 ~ 1e308,15位小数科学计算、数值验证
float1616约6e-5 ~ 65504,3位小数混合精度训练、移动端推理
bfloat1616与float32相同指数范围,8位尾数大规模分布式训练
int6464-9e18 ~ 9e18索引、标签
uint880 ~ 255图像像素存储

3.4 设备管理:CPU与GPU的无缝切换

核心原理

PyTorch通过torch.device抽象统一管理计算设备。CPU设备标识为'cpu',GPU设备标识为'cuda'或'cuda:0'(指定GPU编号)。张量通过.to()方法在设备间迁移,该操作会复制数据(若目标设备不同)。GPU计算的优势在于大规模并行,但数据从CPU到GPU的传输存在PCIe带宽瓶颈(约16GB/s),因此应尽量减少设备间数据传输次数。

设备管理操作

# 检查GPU可用性
print(f"CUDA可用: {torch.cuda.is_available()}")
print(f"GPU数量: {torch.cuda.device_count()}")
print(f"当前GPU: {torch.cuda.current_device()}")

# 定义设备对象
cpu_dev = torch.device('cpu')
gpu_dev = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# 创建张量并迁移
t_cpu = torch.randn(3, 3)
t_gpu = t_cpu.to(gpu_dev)          # CPU → GPU
t_back = t_gpu.cpu()               # GPU → CPU(或使用.to('cpu'))

# 直接在GPU上创建
t_direct = torch.randn(3, 3, device=gpu_dev)

# 模型迁移
model = torch.nn.Linear(10, 10)
model.to(gpu_dev)                  # 模型参数迁移到GPU

案例拆解:训练循环中的设备管理最佳实践

在典型的训练脚本中,设备管理需要贯穿数据加载、模型前向传播和损失计算的全过程。以下是一个完整的训练循环片段:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MyModel().to(device)
optimizer = torch.optim.Adam(model.parameters())

for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        # 关键:将数据迁移到与模型相同的设备
        data, target = data.to(device), target.to(device)
        
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        
        # 定期将损失值移回CPU用于日志记录
        if batch_idx % 100 == 0:
            loss_cpu = loss.item()  # .item()自动转为Python float,无需显式迁移

该案例展示了三个关键实践:①数据与模型必须位于同一设备,否则会报错;②loss.item()会自动将GPU张量转为Python标量,无需手动调用.cpu();③避免在循环内频繁进行设备切换,应一次性将整个批次数据迁移到目标设备。

常见误区与总结

风险提示:当GPU显存不足时,程序会抛出CUDA out of memory错误。此时应检查:①是否在循环中创建了未释放的中间张量;②是否将整个数据集一次性加载到GPU;③是否使用了过大的batch size。使用torch.cuda.empty_cache()可以释放未使用的缓存显存,但不应在训练循环中频繁调用,否则会降低性能。

另一个常见误区是忽略CPU与GPU之间的数据传输开销。例如,在每次迭代中将单个标量从GPU移回CPU(如loss.item())是必要的,但若将整个梯度张量移回CPU则代价高昂。此外,多GPU训练时需注意设备编号——cuda:0和cuda:1是独立的设备,数据不能隐式共享。

本章要点回顾:①张量创建需根据场景选择直接构造、占位初始化或随机采样;②理解shape、stride和storage_offset有助于优化内存操作;③dtype选择需权衡精度与性能,混合精度训练需配合损失缩放;④设备管理应遵循“数据与模型同设备”原则,并尽量减少跨设备数据传输。掌握这些基础后,读者可以进一步学习张量的数学运算与自动微分机制。