PyTorch基础回顾:张量操作、自动求导机制、nn.Module基类、forward方法定义

说实话,每次带新人做项目,我都会先问一句:「PyTorch 基础扎实吗?」

不是故意刁难。而是我踩过太多坑了——有人把张量形状搞错,调试三天找不到 bug;有人不会用自动求导,手写反向传播写到崩溃。嗯,这些我都经历过。

今天咱们就把这四个核心概念捋一遍。别嫌基础,SwiGLU 的实现全靠它们撑着呢。

1. 张量操作:PyTorch 的「乐高积木」

张量是什么?说白了就是多维数组。但 PyTorch 的张量比 NumPy 的 ndarray 多了一个杀手锏——自动求导。

创建张量,我常用的几种方式:

import torch

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

# 全零张量
t2 = torch.zeros(3, 4)

# 随机张量
t3 = torch.randn(2, 3)  # 标准正态分布

# 单位矩阵
t4 = torch.eye(5)

我个人习惯用 torch.randn 初始化权重,效果比均匀分布好一些。不过要注意,SwiGLU 里的门控机制对初始化挺敏感,后面咱们会细说。

形状操作,这里容易翻车:

x = torch.randn(2, 3, 4)

# 重塑形状
x.view(2, 12)      # 注意:view 要求内存连续
x.reshape(2, 12)   # reshape 更灵活,不连续也能用

# 转置
x.T                 # 二维转置
x.permute(2, 0, 1)  # 多维转置,指定维度顺序

# 拼接
a = torch.randn(2, 3)
b = torch.randn(2, 4)
c = torch.cat([a, b], dim=1)  # 沿着列拼接,形状变成 (2, 7)
我曾经踩过的坑:view 和 reshape 的区别。view 要求张量内存连续,否则会报错。reshape 会自动处理不连续的情况,但性能稍差。在自定义模块里,我建议统一用 reshape,省心。

广播机制,新手必看:

a = torch.randn(3, 1)  # 形状 (3, 1)
b = torch.randn(1, 4)  # 形状 (1, 4)
c = a + b              # 结果形状 (3, 4),自动广播

广播的规则很简单:从最后一个维度往前比,要么相等,要么一个是 1。你想想看,这跟 NumPy 完全一致,所以 NumPy 玩得溜的人上手 PyTorch 特别快。

2. 自动求导机制:PyTorch 的「魔法」

自动求导,英文叫 Autograd。说白了就是框架帮你算梯度,你不用手写链式法则了。

核心就三个东西:

  • requires_grad=True:告诉 PyTorch 这个张量需要计算梯度
  • .backward():从某个标量开始反向传播
  • .grad:存着计算好的梯度
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x + 1
y.backward()

print(x.grad)  # 输出 tensor([7.]),因为 dy/dx = 2x + 3 = 7

嗯,这里要注意:backward() 只能对标量调用。如果你的 y 不是标量,得传一个同形状的 gradient 参数。我在做多任务学习时经常用到这个技巧。

我的小习惯:每次调用 backward 之前,记得用 optimizer.zero_grad() 清空梯度。不然梯度会累加,结果就乱套了。我曾经因为这个 bug 调了一整个下午...

计算图是怎么工作的?

PyTorch 会动态构建一个计算图。每次前向传播时,它记录下所有操作;反向传播时,沿着图往回走,用链式法则算梯度。

你想想看,这跟 TensorFlow 1.x 的静态图完全不一样。动态图的好处是:你可以用 if 语句、for 循环,想怎么调试就怎么调试。SwiGLU 的实现里,我们会用到这个特性来做条件计算。

3. nn.Module 基类:所有模型的「祖宗」

在 PyTorch 里,所有神经网络模块都要继承 nn.Module。这不是规定,而是为了方便——它帮你管理参数、处理设备迁移、支持序列化。

一个最简单的自定义模块:

import torch.nn as nn

class MyLinear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(out_features, in_features))
        self.bias = nn.Parameter(torch.zeros(out_features))
    
    def forward(self, x):
        return x @ self.weight.T + self.bias

看到 nn.Parameter 了吗?它告诉 PyTorch:「这个张量是模型参数,需要被优化器更新。」如果你用普通的 torch.tensor,优化器根本不会理它。

nn.Module 帮我做了哪些事?

  • .parameters():自动收集所有可训练参数
  • .to(device):一键迁移到 GPU
  • .state_dict() / .load_state_dict():保存和加载模型
  • .train() / .eval():切换训练/评估模式
重点:在 __init__ 里注册所有子模块和参数。如果你在 forward 里临时创建 nn.Linear,它的参数不会被管理,训练时会出大问题。我见过有人这么干,结果模型死活不收敛...

4. forward 方法定义:模型的前向逻辑

forward 方法是模型的核心。它定义了输入怎么变成输出。你想想看,整个神经网络就是个函数,forward 就是函数体。

写 forward 的几个原则:

  1. 保持简洁:复杂的逻辑拆成子模块,forward 只做组装
  2. 支持动态图:可以用 if、for,PyTorch 会帮你记录计算图
  3. 注意设备一致性:输入和参数要在同一个设备上
class SimpleMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(256, 10)
    
    def forward(self, x):
        # x 的形状: (batch_size, 784)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

我个人习惯在 forward 里加一些断言,检查输入形状。比如:

def forward(self, x):
    assert x.dim() == 2, f"Expected 2D input, got {x.dim()}D"
    assert x.size(1) == 784, f"Expected 784 features, got {x.size(1)}"
    # ... 后续逻辑

这样调试起来特别快。一旦形状不对,程序立刻报错,而不是跑到后面莫名其妙地崩溃。

注意:千万不要在 forward 里修改模型的参数!比如 self.weight.data += 1 这种操作。这会破坏计算图,导致梯度计算错误。如果你需要条件分支,用 if 语句控制计算路径,而不是修改参数。

知识体系总览

下面这张图,是我自己总结的 PyTorch 基础四件套的关系。你看一眼,心里就有谱了:

PyTorch 基础四件套 张量操作 创建、形状变换、广播 索引、切片、数学运算 设备迁移 (CPU/GPU) 自动求导 requires_grad / backward 计算图构建与反向传播 梯度累加与清零 nn.Module 基类 参数管理 (nn.Parameter) 子模块注册与嵌套 保存/加载 (state_dict) forward 方法 前向传播逻辑定义 动态图支持 (if/for) 输入形状校验 requires_grad 构建计算图 nn.Module 管理 forward 中的参数 四者协同:张量承载数据 → 自动求导计算梯度 → nn.Module 管理参数 → forward 定义逻辑

你看,这四个东西是环环相扣的。张量是数据载体,自动求导负责算梯度,nn.Module 帮你管参数,forward 定义前向逻辑。SwiGLU 的实现,就是在这四个基础上搭积木。

好了,基础就复习到这儿。下一章咱们直接上手写 SwiGLU 模块,到时候你会感谢今天打下的基础。


专注资料整理