多层感知机:结构、BatchNorm、Dropout,附 PyTorch 代码

多层感知机(Multi-Layer Perceptron,简称 MLP )是最基础、最常用的前馈神经网络结构之一。它的核心就是把若干个“线性层(全连接)+ 非线性激活函数”堆叠起来,让网络能够拟合复杂的非线性映射。


1. 多层感知机的基本结构

典型的 MLP 由三部分组成:

  1. 输入层(\(l = 0\)):接收特征 \(\mathbf{p}_i \in \mathbb{R}^{d_0}\),其中 \(d_0\) 为特征维度。即 \(a^{(0)}_i = \mathbf{p}_i\)单个样本 \(i\))。
  2. 若干隐藏层(\(l = 1, \cdots, {L-1}\)):
    • 一个典型的“层”包含两步:
      • 线性变换: \[ z^{(l)}_i = W^{(l)}a^{(l-1)}_i + b^{(l)},\quad i = 1, \cdots, B \]
      • 非线性激活: \[ a^{(l)}_i \leftarrow f^{(l)}(z^{(l)}_i) \]
    • 其中:
      • \(B\) 为 batch size
      • \(z^{(l)}_i,a^{(l)}_i \in \mathbb{R}^{d_l}\)
      • \(W^{(l)}\) :第 \(l\) 层的权重矩阵\(W^{(l)} \in \mathbb{R}^{d_l \times d_{l-1}}\)
      • \(b^{(l)}\) :偏置向量,\(b^{(l)} \in \mathbb{R}^{d_l}\)
      • \(f^{(l)}(\cdot)\) :非线性激活函数,\(\mathbb{R}^{d_l} \rightarrow \mathbb{R}^{d_l}\)
    • 通过多层堆叠\[a^{(l)}_i = f^{(l)}(W^{(l)}a^{(l-1)}_i + b^{(l)})\]实现特征变换,通常包含多个全连接层和非线性激活函数。
    • 隐藏层输出维度为 \(d_1, \cdots, d_{L-1}\)
  3. 输出层:
    • 得到预测 \(\hat y\),通常为一个全连接层。
    • 输出维度为 \(d_L\)
    • 分类/回归会用不同输出形式。

如果没有非线性激活(全是线性层),那再多层也等价于“一层线性变换”,表达能力不会变强;非线性是 MLP 能拟合复杂函数的关键。

对一个 batch 输入 \(\mathbf{a}^{(0)} \in \mathbb{R}^{B \times d_0}\),等价于同一个 MLP 被并行地作用在每个样本上。

MLP结构示意

2. 常见非线性激活函数

\[ \mathrm{ReLU}(x)=\max(0,x) \] 优点:简单、收敛快、缓解梯度消失(相对 Sigmoid/Tanh)。

\[ \mathrm{LeakyReLU}(x)=\max(\alpha x,x),\quad \alpha\in(0,1) \] 特点:负半轴保留小斜率,减少“神经元死亡”。

\[ \sigma(x)=\frac{1}{1+e^{-x}} \] 特点:输出在 \((0,1)\) 区间,常用于二分类输出层;隐藏层使用时更容易梯度消失。

\[ \tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}} \] 特点:输出在 \((-1,1)\),比 Sigmoid 零均值,但仍可能梯度消失。

\[ \mathrm{GELU}(x)=x\Phi(x) \] 其中 \(\Phi(x)\) 为标准正态分布的累积分布函数。特点:比 ReLU 更“平滑”。


3. BatchNorm

实现中最常见顺序:Linear -> BN -> ReLU

BatchNorm(批归一化)一般作用在每个隐藏层的线性输出\[ \mathbf{z}^{(l)} = \begin{bmatrix} (z^{(l)}_{1})^\top\\ \vdots\\ (z^{(l)}_{B})^\top \end{bmatrix} = \begin{bmatrix} \big(W^{(l)} a^{(l-1)}_{1}+b^{(l)}\big)^\top\\ \vdots\\ \big(W^{(l)} a^{(l-1)}_{B}+b^{(l)}\big)^\top \end{bmatrix} \in \mathbb{R}^{B\times d_l}. \]

先对每个特征维度 \(j\)(也就是每个神经元通道)在一个 batch 上计算均值和方差: \[ \mu_j^{(l)}=\frac{1}{B}\sum_{i=1}^B z_{i,j}^{(l)},\qquad (\sigma_j^{(l)})^2=\frac{1}{B}\sum_{i=1}^B(z_{i,j}^{(l)}-\mu_j^{(l)})^2 \] 定义标准化: \[ \hat z^{(l)}_{i,j}=\frac{z^{(l)}_{i,j}-\mu_j^{(l)}}{\sqrt{(\sigma_j^{(l)})^2+\varepsilon}}, \] 其中,\(\varepsilon\) 是一个小常数,用于防止除零错误。

再做可学习缩放平移(\(\gamma^{(l)},\beta^{(l)} \in \mathbb{R}^{d_l}\)): \[ \tilde{z}_{i,j}^{(l)} = \gamma_j^{(l)} \hat z^{(l)}_{i,j}+\beta_j^{(l)}, \] 然后再激活: \[ \tilde{a}_i^{(l)} = f^{(l)}(\tilde{z}_i^{(l)}). \]

BatchNorm 的常见作用:

  • 稳定训练、加速收敛
  • 对不同 batch 的统计量带来一定“噪声”,具有一定正则化效果

BatchNorm 对 batch size 较敏感,batch 太小(甚至为 1)时效果会明显变差;这时常用 LayerNorm / GroupNorm。


4. Dropout

实现中最常见顺序:Linear -> BN -> ReLU -> Dropout

Dropout 在训练时随机把一部分样本一部分神经元激活输出置零,避免网络过度依赖少数神经元,从而抑制过拟合。

比如隐藏层激活为: \[ \mathbf{a}^{(l)} = \begin{bmatrix} (a_1^{(l)})^\top\\ \vdots\\ (a_B^{(l)})^\top \end{bmatrix} = \begin{bmatrix} (f(z_1^{(l)}))^\top\\ \vdots\\ (f(z_B^{(l)}))^\top \end{bmatrix} \in\mathbb{R}^{B\times d_l}. \]

Dropout 产生 mask \(m^{(l)}\in \{0,1\}^{B\times d_l}\),然后 \[ h^{(l)}=\frac{m^{(l)}\odot \mathbf{a}^{(l)}}{1-p}. \] 也就是说:被 mask 为 0 的那些元素(某些样本的某些神经元输出)在这一轮前向里直接变成 0。

  • \(m\)(mask)是一个随机 0/1 矩阵(或向量),和 \(\mathbf{a}^{(l)}\) 同形状。
  • 其中,\(m_{i,j}\sim \mathrm{Bernoulli}(1-p)\): 每个 \(m_{i,j}\) 独立地取值 \[ m_{i,j}=\begin{cases} 1,&\text{概率 }1-p \\ 0,&\text{概率 }p \end{cases} \] 这就是伯努利分布(Bernoulli distribution),只有 \(0/1\) 两种结果的最基本随机分布。

为什么要除以 \(1-p\)?这是 “inverted dropout” 的写法:

  • 训练时:把保留下来的激活放大,使得期望不变;
  • 推理时:直接关掉 Dropout(不再随机置零,也不需要缩放,直接使用 \(\mathbf{a}^{(l)}\))。

5. PyTorch 三种结构的初始化代码

下面给出 PyTorch 三种结构的初始化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn

class ClassicMLP(nn.Module):
def __init__(self, hidden_dim=32):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
nn.ReLU(), # 输出也 ReLU(按任务可去掉)
)

def forward(self, x):
return self.net(x)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import torch.nn as nn

class MLPWithBatchNorm(nn.Module):
def __init__(self, hidden_dim=32):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),

nn.Linear(hidden_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),

nn.Linear(hidden_dim, 1),
nn.ReLU(), # 回归任务通常去掉
)

def forward(self, x):
return self.net(x)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import torch.nn as nn

class MLPWithBNAndDropout(nn.Module):
def __init__(self, hidden_dim=32, p_drop=0.2):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(p=p_drop),

nn.Linear(hidden_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(p=p_drop),

nn.Linear(hidden_dim, 1),
nn.ReLU(), # 按任务决定是否保留
)

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

6. 小结:三者区别一句话

  • 经典多层感知机(MLP):Linear + 激活,最基础的非线性拟合器
  • MLP + BatchNorm:在层间加入归一化,训练更稳定、收敛更快
  • MLP + BatchNorm + Dropout:再加随机丢弃,进一步抑制过拟合(但训练噪声更大)