Skip to content

神经网络:从一次前向传播讲起 ​

目标:彻底搞懂「一层网络在做什么」,以及为什么堆叠起来就变强。

不要从「神经元模拟大脑」开始学 —— 那是个好比喻,但不是好入口。从矩阵乘法 + 非线性开始,最快。

一个线性层就是一次矩阵乘法 ​

python
import torch, torch.nn as nn

layer = nn.Linear(in_features=4, out_features=3)
x = torch.randn(8, 4)          # 8 个样本,每个 4 个特征
y = layer(x)                   # → (8, 3)
print(y.shape)                 # torch.Size([8, 3])

内部就是 $y = xW^\top + b$,其中 $W$ 是 (3,4)、$b$ 是 (3,)。

形状记忆法:(B, in) × (in, out) → (B, out)。nn.Linear(in, out) 里的两个数字就是这里。

为什么需要非线性 ​

如果只有线性层,堆 100 层等价于 1 层:

$$W_2(W_1x) = (W_2W_1)x = Wx$$

没有非线性,深度毫无意义。 激活函数就是那个「让堆叠产生表达力」的东西。

激活函数公式特点用在哪
ReLU$\max(0,x)$简单、快、缓解梯度消失隐藏层默认
GELU$x\Phi(x)$平滑版 ReLUTransformer 标配
Sigmoid$\frac{1}{1+e^{-x}}$压到 (0,1)二分类输出
Tanh压到 (-1,1)RNN
Softmax$\frac{e^{z_i}}{\sum e^{z_j}}$变成概率分布多分类输出
python
# 一个完整的最小网络
net = nn.Sequential(
    nn.Linear(784, 256), nn.ReLU(),
    nn.Linear(256, 128), nn.ReLU(),
    nn.Linear(128, 10),            # 10 类,输出 logits(不接 softmax)
)

输出层不要接 Softmax

用 nn.CrossEntropyLoss 时它内部已经包含 log-sum-exp,你输出 logits 即可。 手动加 Softmax 再交给 CrossEntropy = 做了两次,损失会算错。

前向传播:一次完整计算 ​

python
x = torch.randn(32, 784)               # 32 张 28×28 展平的图
h1 = torch.relu(x @ W1 + b1)           # (32, 256)
h2 = torch.relu(h1 @ W2 + b2)          # (32, 128)
logits = h2 @ W3 + b3                  # (32, 10)
probs = torch.softmax(logits, dim=1)   # 概率

每一层做的事:把上一层的表示,变换成一个更有用的表示。

  • 第一层学到边缘、颜色块
  • 中间层学到纹理、局部形状
  • 深层学到物体部件、整体语义

这就是「深度」的意义:逐层抽象。

反向传播:只要记住三件事 ​

  1. 前向时框架记录了计算图(谁由谁算出)
  2. loss.backward() 沿图反向用链式法则求每个参数的梯度
  3. optimizer.step() 用梯度更新参数
python
loss = criterion(net(x), y)
loss.backward()          # 填好每个 param.grad
optimizer.step()         # param -= lr * param.grad
optimizer.zero_grad()    # 清空,为下一轮准备

顺序不能错

zero_grad() 在最前或最后都行,但必须在 backward() 之前清空过一次。 漏了它,梯度会跨 batch 累加 —— 表现为 loss 剧烈震荡或直接 nan。

完整可运行例子:手写数字分类 ​

python
import torch, torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

device = "cuda" if torch.cuda.is_available() else "cpu"

# 数据
tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
train_ds = datasets.MNIST("./data", train=True, download=True, transform=tf)
test_ds = datasets.MNIST("./data", train=False, transform=tf)
train_dl = DataLoader(train_ds, batch_size=128, shuffle=True)
test_dl = DataLoader(test_ds, batch_size=256)

# 模型
model = nn.Sequential(
    nn.Flatten(),
    nn.Linear(784, 256), nn.ReLU(), nn.Dropout(0.2),
    nn.Linear(256, 128), nn.ReLU(),
    nn.Linear(128, 10),
).to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)

for epoch in range(5):
    model.train()
    for xb, yb in train_dl:
        xb, yb = xb.to(device), yb.to(device)
        loss = loss_fn(model(xb), yb)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    # 验证
    model.eval()
    correct = total = 0
    with torch.no_grad():
        for xb, yb in test_dl:
            pred = model(xb.to(device)).argmax(1).cpu()
            correct += (pred == yb).sum().item()
            total += yb.size(0)
    print(f"epoch {epoch+1}: test acc = {correct/total:.4f}")

跑完应看到 97%+。若不到 95%,检查 Normalize、model.train()/eval()、是否 shuffle=True。

参数量与显存估算 ​

python
def count_params(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"{count_params(model)/1e6:.2f}M")

粗算规则:

项目估算
参数显存参数量 × 精度字节(fp32: 4B,fp16: 2B)
优化器状态(Adam)额外 2 × 参数显存
激活值与 batch × 序列长度 × 宽度正比,训练时的大头
经验值训练 7B 模型全参数微调需 ~80GB+;推理 fp16 需 ~14GB

常见误区 ​

误区正解
「层数越多越好」没有残差/归一化,深了反而训不动(见 训练)
「参数量大就更准」数据不够时大模型更容易过拟合
「loss 降到 0 最好」那是记住了训练集,测试集往往崩
「必须懂反向传播推导」要懂「在做什么」,不必手推矩阵导数

通过标准 ​

下一步 ​

→ 训练:损失、优化器与调参

AI 学习路线 · 从入门到专家