MLP(Multilayer Perceptron,多层感知机)是最基础也最重要的神经网络之一。

如果说线性回归、逻辑回归只有一层线性变换,那么 MLP 就是在多层线性变换之间加入非线性激活函数,让模型能够拟合更复杂的关系。

它的结构并不神秘:

输入特征 -> 隐藏层 -> 激活函数 -> 隐藏层 -> 激活函数 -> 输出层

一句话概括:

MLP 用多个全连接层和非线性激活函数,把输入特征一步步变换成适合分类或回归的输出。

MLP 由输入层、隐藏层和输出层组成

1. 为什么需要 MLP

前面学习线性回归时,模型大致是:

$$
\hat{y}=wx+b
$$

对于多维输入,可以写成:

$$
\hat{y}=xW+b
$$

这类模型只能表达线性关系。

例如下面这种问题,线性模型就比较吃力:

两个特征单独看都不能决定类别,
但它们组合起来以后才有意义。

经典例子是 XOR:

$x_1$ $x_2$ 标签
0 0 0
0 1 1
1 0 1
1 1 0

这四个点无法用一条直线完美分开。

MLP 的作用就是:先通过隐藏层把原始特征映射到新的空间,再在新空间里完成分类或回归。

2. MLP 的基本结构

一个最简单的 MLP 可以写成:

$$
h_1 = \sigma(xW_1+b_1)
$$

$$
h_2 = \sigma(h_1W_2+b_2)
$$

$$
\hat{y}=h_2W_3+b_3
$$

其中:

  • $x$ 是输入特征。
  • $W_1,W_2,W_3$ 是权重矩阵。
  • $b_1,b_2,b_3$ 是偏置。
  • $\sigma$ 是非线性激活函数。
  • $h_1,h_2$ 是隐藏层表示。

如果是分类任务,最后通常会输出每个类别的 logits,再接 softmax 或交叉熵损失。

如果是回归任务,最后通常直接输出一个连续数值。

MLP 单层计算可以拆成线性变换和激活函数

3. 为什么一定要激活函数

如果没有激活函数,多层线性层叠在一起仍然只是线性变换。

例如:

$$
h=xW_1+b_1
$$

$$
y=hW_2+b_2
$$

代入得到:

$$
y=(xW_1+b_1)W_2+b_2
$$

展开:

$$
y=x(W_1W_2)+(b_1W_2+b_2)
$$

这仍然可以看成:

$$
y=xW+b
$$

也就是说,如果没有非线性激活函数,堆再多层也没有本质提升。

激活函数的作用就是打破线性,让网络可以拟合复杂曲线、复杂边界和复杂特征组合。

常见激活函数包括:

激活函数 公式或含义 常见用途
ReLU $\max(0,x)$ 默认常用,简单高效
Sigmoid 输出在 $(0,1)$ 二分类概率、门控结构
Tanh 输出在 $(-1,1)$ 早期神经网络、RNN
GELU 平滑非线性 Transformer 中常见
Softmax 多类别概率归一化 多分类输出解释

在 MLP 的隐藏层中,最常见的是 ReLU 或 GELU。

4. 手工算一次 MLP 前向传播

为了直观理解 MLP,我们手工算一个很小的网络。

假设输入为:

$$
x=[2,3]
$$

第一层有两个神经元:

$$
W_1=
\begin{bmatrix}
1 & -1 \
2 & 1
\end{bmatrix},
\quad
b_1=[0,1]
$$

先算线性部分:

$$
z_1=xW_1+b_1
$$

逐项计算:

$$
z_{1,1}=2\times1+3\times2+0=8
$$

$$
z_{1,2}=2\times(-1)+3\times1+1=2
$$

所以:

$$
z_1=[8,2]
$$

经过 ReLU:

$$
h_1=\text{ReLU}([8,2])=[8,2]
$$

第二层输出一个数:

$$
W_2=
\begin{bmatrix}
1 \
-2
\end{bmatrix},
\quad
b_2=0.5
$$

则:

$$
\hat{y}=h_1W_2+b_2=8\times1+2\times(-2)+0.5=4.5
$$

这就是一次完整前向传播:

输入 [2, 3]
-> 第一层线性变换 [8, 2]
-> ReLU [8, 2]
-> 第二层线性变换 4.5

如果某个中间值是负数,ReLU 会把它变成 0,这就是非线性发生的地方。

5. MLP 如何训练

MLP 的训练流程和其他神经网络类似:

  1. 前向传播:输入样本,得到预测值。
  2. 计算损失:比较预测值和真实标签。
  3. 反向传播:根据损失计算每个参数的梯度。
  4. 参数更新:优化器根据梯度更新权重和偏置。
  5. 重复很多轮,直到模型效果稳定。

PyTorch 训练循环的核心步骤

用公式概括,目标是最小化损失函数:

$$
\min_\theta \mathcal{L}(\hat{y},y)
$$

其中 $\theta$ 表示所有可学习参数。

参数更新可以写成:

$$
\theta := \theta-\alpha\nabla_\theta \mathcal{L}
$$

这和梯度下降的思想完全一致,只是神经网络里的参数更多,计算图更复杂。

6. 用 PyTorch 实现一个 MLP

下面用 PyTorch 写一个二分类 MLP。

假设输入有 2 个特征,输出有 2 个类别:

import torch
from torch import nn


class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 16),
            nn.ReLU(),
            nn.Linear(16, 16),
            nn.ReLU(),
            nn.Linear(16, 2),
        )

    def forward(self, x):
        return self.net(x)


model = MLP()

x = torch.tensor([[2.0, 3.0]])
logits = model(x)

print(logits)

这里的输出 logits 不是概率,而是模型对每个类别给出的原始分数。

如果想转成概率,可以使用:

prob = torch.softmax(logits, dim=1)
print(prob)

但训练时如果使用 nn.CrossEntropyLoss,通常不需要自己先手动 softmax,因为它内部会处理 logits。

7. 完整训练例子:拟合 XOR

XOR 是理解 MLP 的经典小例子。

import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset


torch.manual_seed(42)

X = torch.tensor([
    [0.0, 0.0],
    [0.0, 1.0],
    [1.0, 0.0],
    [1.0, 1.0],
])

y = torch.tensor([0, 1, 1, 0])

dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=4, shuffle=True)


class XORMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 8),
            nn.Tanh(),
            nn.Linear(8, 2),
        )

    def forward(self, x):
        return self.net(x)


model = XORMLP()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)

for epoch in range(1000):
    for batch_x, batch_y in loader:
        logits = model(batch_x)
        loss = criterion(logits, batch_y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    if epoch % 200 == 0:
        print(f"epoch={epoch}, loss={loss.item():.4f}")

model.eval()
with torch.no_grad():
    logits = model(X)
    pred = torch.argmax(logits, dim=1)
    print("prediction:", pred.tolist())

如果训练正常,最终输出大致会接近:

prediction: [0, 1, 1, 0]

这个例子说明:MLP 通过隐藏层和非线性激活函数,可以学到线性模型无法直接表达的决策边界。

8. PyTorch 神经网络组件总览

PyTorch 官方基础教程把机器学习工作流拆成数据、模型、优化和保存等环节;写神经网络时,常见组件也基本围绕这些环节展开。

PyTorch 神经网络常用组件

常用模块可以先按功能分组:

组件 作用
torch.Tensor 存储数据和梯度相关信息
nn.Module 所有神经网络模块的基类
nn.Linear 全连接层,也就是仿射变换
nn.ReLUnn.Sigmoidnn.GELU 激活函数
nn.Sequential 顺序堆叠多个层
nn.CrossEntropyLossnn.MSELoss 损失函数
torch.optim.SGDtorch.optim.Adam 优化器
DatasetDataLoader 数据集和小批量加载
model.train()model.eval() 切换训练和推理模式
torch.no_grad() 推理时关闭梯度记录

下面逐个解释常用函数和它们背后的原理。

9. torch.Tensor:数据的基本容器

Tensor 可以理解为 PyTorch 里的多维数组。

import torch

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
print(x.shape)  # torch.Size([2, 2])

如果设置:

x.requires_grad_(True)

PyTorch 就会追踪后续操作,用于自动求导。

例如:

x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x
y.backward()
print(x.grad)

手工计算:

$$
y=x^2+3x
$$

$$
\frac{dy}{dx}=2x+3
$$

当 $x=2$ 时:

$$
\frac{dy}{dx}=7
$$

所以 x.grad 会是 7。

10. nn.Module:神经网络的基本单元

在 PyTorch 中,几乎所有模型和层都继承自 nn.Module

你通常需要做两件事:

  1. __init__ 中定义层。
  2. forward 中定义前向传播逻辑。

例如:

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(2, 4)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(4, 2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

nn.Module 会自动管理子模块和参数。

model = SimpleNet()

for name, param in model.named_parameters():
    print(name, param.shape)

可能输出:

fc1.weight torch.Size([4, 2])
fc1.bias torch.Size([4])
fc2.weight torch.Size([2, 4])
fc2.bias torch.Size([2])

这就是为什么优化器可以直接写:

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

因为 model.parameters() 会返回模型中所有可训练参数。

11. nn.Linear:全连接层

PyTorch 官方文档中,nn.Linear 表示对输入做仿射线性变换:

$$
y=xA^T+b
$$

其中:

  • $x$ 是输入。
  • $A$ 是权重矩阵。
  • $b$ 是偏置。

例子:

layer = nn.Linear(3, 2)

x = torch.tensor([[1.0, 2.0, 3.0]])
y = layer(x)

print(y.shape)  # torch.Size([1, 2])

如果输入形状是:

[batch_size, in_features]

输出形状就是:

[batch_size, out_features]

例如:

nn.Linear(3, 2)

表示每个样本有 3 个输入特征,经过这一层后变成 2 个输出特征。

它内部可学习的参数包括:

  • weight:形状为 [out_features, in_features]
  • bias:形状为 [out_features]

12. 激活函数:ReLUSigmoidTanhGELU

12.1 ReLU

ReLU 是最常见的隐藏层激活函数:

$$
\text{ReLU}(x)=\max(0,x)
$$

PyTorch 写法:

relu = nn.ReLU()
x = torch.tensor([-2.0, 0.5, 3.0])
print(relu(x))

输出:

tensor([0.0000, 0.5000, 3.0000])

12.2 Sigmoid

Sigmoid 常用于二分类概率或门控结构:

$$
\sigma(x)=\frac{1}{1+e^{-x}}
$$

sigmoid = nn.Sigmoid()
print(sigmoid(torch.tensor([0.0])))

输出为 0.5。

12.3 Tanh

Tanh 输出范围是 $(-1,1)$:

tanh = nn.Tanh()
print(tanh(torch.tensor([0.0])))

输出为 0。

12.4 GELU

GELU 是 Transformer 中常见的激活函数,比 ReLU 更平滑。

gelu = nn.GELU()
x = torch.randn(4)
print(gelu(x))

对于普通 MLP,ReLU 通常已经足够;如果是更深的网络或 Transformer 风格模型,可以考虑 GELU。

13. nn.Sequential:顺序堆叠网络

如果模型就是一层接一层地执行,可以用 nn.Sequential 简化代码:

model = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 16),
    nn.ReLU(),
    nn.Linear(16, 2),
)

它等价于:

x -> Linear -> ReLU -> Linear -> ReLU -> Linear

适合结构简单的 MLP。

如果模型中有分支、多个输入、条件判断,就更适合自定义 nn.Module 并手写 forward

14. 损失函数:模型到底错在哪里

损失函数用于衡量预测值和真实标签之间的差距。

14.1 nn.MSELoss

均方误差常用于回归:

$$
\text{MSE}=\frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i-y_i)^2
$$

loss_fn = nn.MSELoss()
pred = torch.tensor([2.5, 3.0])
target = torch.tensor([2.0, 4.0])
loss = loss_fn(pred, target)
print(loss)

手工计算:

$$
\frac{(2.5-2.0)^2+(3.0-4.0)^2}{2}
=
\frac{0.25+1}{2}
=0.625
$$

14.2 nn.CrossEntropyLoss

交叉熵常用于多分类。

PyTorch 官方文档说明,CrossEntropyLoss 用于计算输入 logits 和目标标签之间的交叉熵;当目标是类别索引时,它等价于先做 LogSoftmax,再做 NLLLoss

所以训练多分类模型时,通常这样写:

criterion = nn.CrossEntropyLoss()
logits = model(x)
loss = criterion(logits, y)

不要提前写:

prob = torch.softmax(logits, dim=1)
loss = criterion(prob, y)  # 不推荐

因为 CrossEntropyLoss 期望输入是未归一化的 logits。

一个简单例子:

logits = torch.tensor([[2.0, 1.0, 0.1]])
target = torch.tensor([0])

criterion = nn.CrossEntropyLoss()
loss = criterion(logits, target)
print(loss)

这里标签 0 表示正确类别是第 0 类。第 0 类 logit 最大,所以损失会相对较小。

14.3 nn.BCEWithLogitsLoss

二分类任务常用 BCEWithLogitsLoss

它把 sigmoid 和二元交叉熵结合到一起,数值稳定性比手动 Sigmoid + BCELoss 更好。

criterion = nn.BCEWithLogitsLoss()
logit = torch.tensor([0.8])
target = torch.tensor([1.0])
loss = criterion(logit, target)

如果是单标签二分类,也可以用两个输出神经元加 CrossEntropyLoss;如果是多标签分类,通常用 BCEWithLogitsLoss

15. 自动求导:loss.backward()

PyTorch 的自动求导系统叫 autograd。

在前向传播时,PyTorch 会记录张量之间的计算关系;调用 loss.backward() 时,它会根据链式法则自动计算梯度。

Autograd 会记录前向计算图并在 backward 时计算梯度

例子:

x = torch.tensor(2.0, requires_grad=True)
w = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)

y = w * x + b
loss = (y - 10) ** 2

loss.backward()

print(w.grad)
print(b.grad)

手工理解:

$$
y=wx+b=3\times2+1=7
$$

$$
loss=(7-10)^2=9
$$

对 $w$ 求导:

$$
\frac{\partial loss}{\partial w}
=
2(y-10)\frac{\partial y}{\partial w}
=
2(7-10)\times2
=-12
$$

对 $b$ 求导:

$$
\frac{\partial loss}{\partial b}
=
2(y-10)\frac{\partial y}{\partial b}
=
2(7-10)\times1
=-6
$$

所以 w.grad-12b.grad-6

16. 优化器:SGDAdam

优化器负责根据梯度更新参数。

最基础的是 SGD:

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

它的思想是:

$$
\theta := \theta-\alpha\nabla_\theta \mathcal{L}
$$

Adam 更常用:

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

Adam 会维护梯度的一阶矩估计和二阶矩估计,可以理解为同时考虑“梯度方向”和“梯度变化尺度”,通常比普通 SGD 更省心。

典型训练步骤是:

optimizer.zero_grad()
loss.backward()
optimizer.step()

为什么要 zero_grad()

因为 PyTorch 默认会累积梯度。如果不清零,下一次 backward() 的梯度会加到上一次梯度上。

这在某些梯度累积场景有用,但普通训练中通常每个 batch 都要清零。

17. DatasetDataLoader

神经网络通常使用 mini-batch 训练。

PyTorch 中:

  • Dataset 负责定义“如何取一个样本”。
  • DataLoader 负责批量加载、打乱和并行读取。

最简单可以用 TensorDataset

from torch.utils.data import TensorDataset, DataLoader

X = torch.randn(100, 2)
y = torch.randint(0, 2, (100,))

dataset = TensorDataset(X, y)
loader = DataLoader(
    dataset,
    batch_size=16,
    shuffle=True,
)

for batch_x, batch_y in loader:
    print(batch_x.shape, batch_y.shape)
    break

输出形状大致是:

torch.Size([16, 2]) torch.Size([16])

这说明每次取出 16 个样本。

18. train()eval()no_grad()

训练和推理时,模型行为可能不同。

例如:

  • Dropout 训练时会随机丢弃部分神经元,推理时不会。
  • BatchNorm 训练时使用当前 batch 统计量,推理时使用累计统计量。

所以训练时要写:

model.train()

验证或推理时要写:

model.eval()

推理时还应关闭梯度记录:

model.eval()
with torch.no_grad():
    logits = model(x)
    pred = torch.argmax(logits, dim=1)

这样可以减少显存和计算开销。

19. Dropout 和 BatchNorm

19.1 Dropout

Dropout 是一种正则化方法。训练时,它会随机把一部分神经元输出置 0,减少模型对某些特征的过度依赖。

nn.Dropout(p=0.5)

表示训练时每个元素有 50% 概率被置 0。

常见写法:

self.net = nn.Sequential(
    nn.Linear(20, 64),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(64, 2),
)

19.2 BatchNorm

BatchNorm 会对一批样本的中间激活做标准化,再学习缩放和平移参数。

nn.BatchNorm1d(64)

常见写法:

self.net = nn.Sequential(
    nn.Linear(20, 64),
    nn.BatchNorm1d(64),
    nn.ReLU(),
    nn.Linear(64, 2),
)

它可以让训练更稳定,但小 batch 或分布变化明显时要谨慎使用。

20. 保存和加载模型

训练完模型后,通常保存参数字典:

torch.save(model.state_dict(), "mlp.pth")

加载时:

model = MLP()
model.load_state_dict(torch.load("mlp.pth"))
model.eval()

state_dict 本质上是一个字典,保存了每一层的权重和偏置。

相比直接保存整个模型对象,保存 state_dict 更常见,也更灵活。

21. 常见错误和排查

21.1 CrossEntropyLoss 前手动 softmax

错误写法:

prob = torch.softmax(logits, dim=1)
loss = nn.CrossEntropyLoss()(prob, y)

正确写法:

loss = nn.CrossEntropyLoss()(logits, y)

21.2 标签类型不对

CrossEntropyLoss 的类别索引标签应是 LongTensor

y = y.long()

如果标签是 float,可能会报错或行为不符合预期。

21.3 忘记清空梯度

普通训练中,每个 batch 前要:

optimizer.zero_grad()

否则梯度会累积。

21.4 训练和推理模式没切换

训练:

model.train()

验证:

model.eval()

尤其是模型中有 Dropout 或 BatchNorm 时,这一点很重要。

21.5 维度不匹配

nn.Linear(in_features, out_features)in_features 必须和输入最后一维一致。

例如输入是:

[batch_size, 20]

第一层就应该是:

nn.Linear(20, hidden_dim)

22. MLP 的优点和局限

MLP 的优点:

  • 结构简单,容易理解。
  • 适合表格数据、向量特征和小型任务。
  • 可以作为很多复杂模型的基础模块。
  • PyTorch 实现非常直接。

MLP 的局限:

  • 对图片、序列、图这类有结构的数据,不能天然利用局部关系或拓扑关系。
  • 参数量可能较大。
  • 对特征缩放比较敏感。
  • 缺少 CNN、RNN、Transformer、GNN 那样的结构先验。

所以:

表格/向量特征:MLP 常常是强 baseline。
图片:CNN 或 ViT 往往更合适。
序列:RNN、Transformer 更自然。
图数据:GNN 更适合。

23. 总结

MLP 是理解深度学习和 PyTorch 的好入口。

它的核心可以概括为:

线性层负责特征变换,
激活函数负责引入非线性,
损失函数衡量预测错误,
自动求导计算梯度,
优化器更新参数。

从公式上看,一层 MLP 就是:

$$
h=\sigma(xW+b)
$$

多层堆叠后,就能表达比线性模型复杂得多的函数。

从 PyTorch 角度看,最重要的是掌握这条训练主线:

logits = model(x)
loss = criterion(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

理解这几行代码背后的原理后,再学习 CNN、RNN、Transformer、GNN 或强化学习里的神经网络,都会顺很多。

参考文献

本文主要参考了以下资料:

  1. PyTorch Documentation, torch.nn.Linear: https://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html
  2. PyTorch Documentation, torch.nn.CrossEntropyLoss: https://docs.pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
  3. PyTorch Tutorials, Learn the Basics: https://docs.pytorch.org/tutorials/beginner/basics/intro.html
  4. PyTorch Tutorials, Automatic Differentiation with torch.autograd: https://docs.pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html
  5. PyTorch Tutorials, Optimizing Model Parameters: https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html