多层感知机:结构、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)})\]实现特征变换,通常包含多个全连接层和非线性激活函数。
      • 全连接(fully connected):前一层的每个神经元,都和后一层的每个神经元相连
      • 如果前一层有 \(n\) 个神经元,后一层有 \(m\) 个神经元,那么这一层总共有:\(m \times n\) 个权重 + \(m\) 个偏置,总共 \(m(n+1)\) 个参数。
      • 全连接层参数量通常比较大。
      • 在 PyTorch 里通常对应 nn.Linear
    • 隐藏层输出维度为 \(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 :一个面向深度学习、可运行在 CPU 和 GPU 上的优化张量库
    • 不只是写神经网络的库
    • 而是一整套从张量计算、自动求导、网络搭建、优化训练到数据加载的生态。

下面给出 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)

小结:三者区别一句话

  • 经典多层感知机(MLP):Linear + 激活,最基础的非线性拟合器
  • MLP + BatchNorm:在层间加入归一化,训练更稳定、收敛更快
  • MLP + BatchNorm + Dropout:再加随机丢弃,进一步抑制过拟合(但训练噪声更大)
  • super() 的作用:调用父类的方法。
    • 使用 super().__init__() 执行父类 nn.Module 的初始化方法。

每一层隐藏层维度都可以不同,下面给出相应的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
import torch.nn as nn

class ClassicMLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim):
super().__init__()

if not isinstance(hidden_dims, (list, tuple)) or len(hidden_dims) == 0:
raise ValueError("hidden_dims 必须是非空的 list 或 tuple,例如 [32, 64, 32]")

layers = []
dims = [input_dim] + list(hidden_dims)

# 构造所有隐藏层
for i in range(len(hidden_dims)):
layers.append(nn.Linear(dims[i], dims[i + 1]))
layers.append(nn.ReLU())

# 输出层
layers.append(nn.Linear(hidden_dims[-1], output_dim))

# 若任务需要,也可以在输出层后再加激活函数
# layers.append(nn.ReLU())

self.net = nn.Sequential(*layers)

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
24
25
26
27
28
29
30
import torch
import torch.nn as nn

class MLPWithBatchNorm(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim):
super().__init__()

if not isinstance(hidden_dims, (list, tuple)) or len(hidden_dims) == 0:
raise ValueError("hidden_dims 必须是非空的 list 或 tuple,例如 [32, 64, 32]")

layers = []
dims = [input_dim] + list(hidden_dims)

# 隐藏层:Linear -> BatchNorm -> ReLU
for i in range(len(hidden_dims)):
layers.append(nn.Linear(dims[i], dims[i + 1]))
layers.append(nn.BatchNorm1d(dims[i + 1]))
layers.append(nn.ReLU())

# 输出层一般不加 BatchNorm
layers.append(nn.Linear(hidden_dims[-1], output_dim))

# 回归任务通常不加输出激活
# 分类任务可按需要加 Sigmoid / Softmax(通常更常在损失函数外处理)
# layers.append(nn.ReLU())

self.net = nn.Sequential(*layers)

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
24
25
26
27
28
29
30
import torch
import torch.nn as nn

class MLPWithBNAndDropout(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim, p_drop=0.2):
super().__init__()

if not isinstance(hidden_dims, (list, tuple)) or len(hidden_dims) == 0:
raise ValueError("hidden_dims 必须是非空的 list 或 tuple,例如 [32, 64, 32]")

layers = []
dims = [input_dim] + list(hidden_dims)

# 隐藏层:Linear -> BatchNorm -> ReLU -> Dropout
for i in range(len(hidden_dims)):
layers.append(nn.Linear(dims[i], dims[i + 1]))
layers.append(nn.BatchNorm1d(dims[i + 1]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(p=p_drop))

# 输出层
layers.append(nn.Linear(hidden_dims[-1], output_dim))

# 按任务决定是否添加输出激活
# layers.append(nn.ReLU())

self.net = nn.Sequential(*layers)

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

  • nn.Sequential 的作用:顺序容器。把若干层按顺序放进去,输入会自动一层一层往后传。
    • 输入:扁平的层序列,而不是“列表套列表”。
    • *layers 里的 * 表示 解包

神经网络的维度:

1
2
3
input_dim = 1
hidden_dims = [32, 64, 16]
output_dim = 1

那么:

1
dims = [1, 32, 64, 16]

循环时就会依次生成:

1
2
3
nn.Linear(1, 32)
nn.Linear(32, 64)
nn.Linear(64, 16)

最后再接:

1
nn.Linear(16, 1)

所以整个网络就是:

\[ 1 \to 32 \to 64 \to 16 \to 1 \]

这就明确体现了:

  • 第 1 个隐藏层是 32 维
  • 第 2 个隐藏层是 64 维
  • 第 3 个隐藏层是 16 维

使用示例

1
2
3
4
5
model = ClassicMLP(
input_dim=10,
hidden_dims=[64, 128, 32],
output_dim=2
)

这表示网络为:

\[ 10 \to 64 \to 128 \to 32 \to 2 \]

如果输入 x 的形状是:

1
x.shape == [batch_size, 10]

那么输出就是:

1
y.shape == [batch_size, 2]

在很多任务里,神经网络的最后一层常常先写成 Linear,然后是否再接激活函数,要看任务类型。

依据:输出空间的数学性质和损失函数的搭配。

例如输出一个实数、多个连续值:

1
y = model(x)

通常最后一层直接是 Linear,后面不加激活

因为回归值可能是任意实数。

例如:

1
nn.Linear(hidden_dim, output_dim)

若输出类别 0/1,常见有两种写法:

写法 A:最后层是 Linear,不手动加 Sigmoid 训练时配合:

1
nn.BCEWithLogitsLoss()

写法 B:最后层后面手动加 Sigmoid

把输出压到 (0,1),作为概率。

通常最后一层仍然是:

1
nn.Linear(hidden_dim, num_classes)

输出每个类别的 score,也就是 logits。

训练时一般直接配:

1
nn.CrossEntropyLoss()

这时通常不要手动再加 Softmax 因为 CrossEntropyLoss 内部已经处理了。

如果希望输出满足某种范围,就可能不用 Linear 结尾。

例如:

  • 输出必须在 (0,1):可考虑 Sigmoid
  • 输出必须在 (-1,1):可考虑 Tanh
  • 输出必须大于 0:可考虑 Softplusexp

6. 参数(parameter)与超参数(hyperparameter)

6.1 统计学角度

“超”这里对应英文 hyper-,意思不是“超级厉害”,而是“更高一层”。

hyperparameter 这个词最早在统计学、尤其贝叶斯统计里很常见。

比如:

  • 一个分布有参数
  • 而这个分布参数本身又来自另一个分布
  • 这个更高层分布的参数就叫 hyperparameters

所以“hyper-”本来就带有“上一层、管下一层”的意思。

后来机器学习把这个说法沿用了过来。

  • 参数(parameters):模型内部的可学习变量
  • 超参数(hyperparameters):决定参数如何被学习、或者决定参数空间长什么样的设置

所以超参数比普通参数“高一层”。

超参数是在训练前或训练过程中人为设定的那些量,它们通常不直接通过标准训练过程自动学出来,而是决定:

  • 模型怎么学
  • 学多快
  • 学多久
  • 模型长什么样

常见例子有:

  • 学习率 learning rate
  • batch size
  • 训练轮数 epochs
  • 隐藏层数
  • 每层神经元个数
  • dropout 比例
  • 权重衰减系数
  • 优化器类型

这些量不属于网络内部“被拟合”的权重,但会显著影响最终学到的权重。 不能通过与参数训练相同的步骤直接得到,通常需要:

  • 人工经验设定
  • 网格搜索
  • 随机搜索
  • 贝叶斯优化
  • 验证集调参

6.2 优化角度

从优化角度看,神经网络训练常常像两层问题:

  1. 内层:固定超参数后,优化模型参数 \(\theta\)(如权重 W、偏置 b、BatchNorm 的 \(\gamma,\beta\) 等),使得损失函数 \(L(\theta)\) 最小。即
    • \(\theta^* = \arg\min_\theta L(\theta;\lambda)\)
    • 其中
      • \(\theta\) :模型参数,能通过反向传播、梯度下降从数据中自动学出来
      • \(\lambda\) :超参数
    • 内层优化是针对模型参数 \(\theta\) 的优化
  2. 外层:选择超参数 \(\lambda\)(如学习率、隐藏层维度、Dropout 概率等),使得模型在验证集上表现最好。即:
    • \(\lambda^* = \arg\min_\lambda L_{\text{val}}(\theta^*(\lambda))\)
    • 其中
      • \(L_{\text{val}}\) :验证集上的损失函数
      • \(\theta^*(\lambda)\) :内层优化得到的最优参数,依赖于超参数 \(\lambda\)
    • 外层优化是针对超参数 \(\lambda\) 的优化。
    • 所以超参数其实是控制参数解的更上层变量,因此用 hyper 这个名字。

6.3 超参数并不是绝对不能学

现在有些方法可以自动调整一部分超参数,比如:

  • 学习率调度
  • 神经架构搜索
  • 元学习
  • 可学习的温度系数、正则系数

所以“超参数”是说它不属于标准反向传播直接优化的那组模型参数,或者说它处在更外层。