4、PyTorch核心概念:张量(Tensor)的创建与运算、自动求导机制(Autograd)、GPU加速

好,咱们进入PyTorch最核心的部分。说实话,我刚开始接触PyTorch时,就被它的设计哲学吸引了——动态计算图,用起来特别顺手。这一章我会把张量、自动求导和GPU加速这三个基石讲透。你想想看,搞深度学习,本质上就是在跟这三样东西打交道。

4.1 张量(Tensor):深度学习的基本单元

张量是什么?说白了就是多维数组。0维是标量,1维是向量,2维是矩阵,3维以上统称张量。在PyTorch里,torch.Tensor就是一切的基础。

4.1.1 创建张量的几种方式

我个人习惯用这几种方式创建张量,你记一下:

import torch

# 从列表创建
t1 = torch.tensor([1, 2, 3])
print(t1)  # tensor([1, 2, 3])

# 全零张量
t2 = torch.zeros(3, 4)
print(t2.shape)  # torch.Size([3, 4])

# 随机张量
t3 = torch.randn(2, 3)  # 标准正态分布
t4 = torch.rand(2, 3)   # 均匀分布[0,1)

# 指定数据类型
t5 = torch.ones(2, 2, dtype=torch.float32)
t6 = torch.eye(3)  # 单位矩阵
我的小习惯:创建张量时,我总喜欢显式指定 dtype。为什么?因为默认是 float32,但有时候你需要 float16 来节省显存。我在项目中遇到过因为类型不匹配导致的诡异bug,排查了半天才发现是精度问题。

4.1.2 张量的基本运算

运算这块,记住一个原则:逐元素运算矩阵运算要分清楚。

a = torch.tensor([[1, 2], [3, 4]])
b = torch.tensor([[5, 6], [7, 8]])

# 逐元素运算
c = a + b      # 加法
d = a * b      # 逐元素乘法(不是矩阵乘法!)
e = torch.sigmoid(a)  # 激活函数

# 矩阵运算
f = torch.mm(a, b)    # 矩阵乘法
g = a @ b             # 等价写法,更简洁

# 维度操作
h = a.view(4, 1)      # 重塑形状
i = a.unsqueeze(0)    # 增加维度
j = a.squeeze()       # 压缩维度

嗯,这里要注意:viewreshape 的区别。我踩过这个坑——view 要求张量在内存中是连续的,而 reshape 不要求。如果你对非连续张量用了 view,会报错。所以我现在习惯用 reshape,省心。

4.2 自动求导机制(Autograd):反向传播的引擎

自动求导是PyTorch最牛的地方。你只需要定义前向计算,梯度自动帮你算好。为什么会这样?因为PyTorch会记录所有操作,构建一个计算图

4.2.1 核心概念:requires_grad

只有设置了 requires_grad=True 的张量,才会被追踪计算历史。我刚开始学的时候,经常忘记设置这个,导致梯度为None。

x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()

# 反向传播
out.backward()

# 查看梯度
print(x.grad)
# tensor([[4.5000, 4.5000],
#         [4.5000, 4.5000]])
关键理解: backward() 只计算叶子节点(即你直接创建的张量)的梯度。中间变量的梯度默认不保留,除非你用 retain_graph=True

4.2.2 计算图的动态特性

PyTorch的计算图是动态的。每次前向传播都会重新构建。这意味着你可以在每次迭代中改变网络结构。我在做循环神经网络时,这个特性帮了大忙——不同长度的序列可以共用同一个模型。

# 动态图的例子
for i in range(3):
    if i % 2 == 0:
        y = x * 2
    else:
        y = x + 2
    y.backward()
    print(x.grad)
    x.grad.zero_()  # 记得清零梯度!
我曾经踩过的坑:梯度累加!PyTorch默认不会自动清零梯度。如果你在循环中多次调用 backward(),梯度会累加。所以每次反向传播后,记得调用 optimizer.zero_grad()x.grad.zero_()

4.2.3 禁用梯度追踪

有些场景不需要梯度,比如模型推理或特征提取。用 torch.no_grad() 可以节省显存和加速。

# 模型推理时
model.eval()
with torch.no_grad():
    predictions = model(test_data)

# 或者用装饰器
@torch.no_grad()
def evaluate(model, data):
    return model(data)

4.3 GPU加速:让训练飞起来

没有GPU,深度学习寸步难行。PyTorch对GPU的支持非常优雅——一行代码就能把张量搬到GPU上。

4.3.1 检查GPU可用性

# 检查是否有GPU
if torch.cuda.is_available():
    device = torch.device("cuda")
    print(f"使用GPU: {torch.cuda.get_device_name(0)}")
else:
    device = torch.device("cpu")
    print("使用CPU")

# 查看GPU数量
print(f"GPU数量: {torch.cuda.device_count()}")

4.3.2 张量在CPU和GPU间迁移

# 创建张量
x = torch.randn(1000, 1000)

# 搬到GPU
x_gpu = x.to(device)

# 或者直接在GPU上创建
y = torch.randn(1000, 1000, device=device)

# 搬回CPU
x_cpu = x_gpu.cpu()

# 注意:不同设备上的张量不能直接运算
# z = x + y  # 会报错!x在CPU,y在GPU
我的经验:尽量在数据加载阶段就把数据搬到GPU。不要在每次迭代中都调用 .to(device),那样会拖慢速度。我习惯在 DataLoadercollate_fn 中统一处理。

4.3.3 模型迁移与多GPU训练

# 定义模型
model = MyModel()

# 将模型搬到GPU
model = model.to(device)

# 多GPU训练(单机多卡)
if torch.cuda.device_count() > 1:
    print(f"使用 {torch.cuda.device_count()} 个GPU")
    model = torch.nn.DataParallel(model)

# 前向传播
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
注意:使用 DataParallel 时,模型的参数名会多一个 module. 前缀。保存模型时,如果你用 model.state_dict(),加载时要注意处理这个前缀。我一般保存时去掉 module.,或者直接用 model.module.state_dict()

4.4 实战小技巧:混合精度训练

最后分享一个我常用的技巧——混合精度训练。用 torch.cuda.amp 可以大幅提升训练速度,同时减少显存占用。

from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

for data, labels in dataloader:
    data, labels = data.to(device), labels.to(device)
    
    optimizer.zero_grad()
    
    # 自动混合精度
    with autocast():
        outputs = model(data)
        loss = criterion(outputs, labels)
    
    # 梯度缩放,防止下溢
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

嗯,这一章的内容就到这里。张量是基础,自动求导是核心,GPU加速是保障。把这三点吃透了,后面的模型搭建就会顺畅很多。下一章我们开始搭建第一个真正的卷积神经网络。