从单层感知机到多层感知机(MLP)的演进是神经网络理论的核心突破。

本课将详细阐述多层感知机的完整知识体系,包括其数学动机、网络结构、前向传播、反向传播算法、激活函数、损失函数以及训练技巧,并通过代码示例进行说明 。


一、 多层感知机的核心动机与数学结构

单层感知机的根本局限在于其线性决策边界,无法解决异或(XOR)等线性不可分问题 。多层感知机通过引入一个或多个隐藏层非线性激活函数来解决此问题。

1. 网络结构
一个典型的多层感知机由输入层、至少一个隐藏层和输出层构成。其数学结构可以描述为:

  • 输入层:接收特征向量 $x \in \mathbb{R}^{n}$。
  • 隐藏层:第 $l$ 个隐藏层进行线性变换并施加非线性激活:$h^{(l)} = \sigma^{(l)}(W^{(l)} h^{(l-1)} + b^{(l)})$,其中 $h^{(0)} = x$。$W^{(l)}$ 是权重矩阵,$b^{(l)}$ 是偏置向量,$\sigma^{(l)}$ 是非线性激活函数 。
  • 输出层:根据任务类型(如分类、回归)选择输出变换。对于多分类,常使用Softmax函数:$\hat{y} = \text{Softmax}(W^{(L)} h^{(L-1)} + b^{(L)})$ 。

2. 前向传播公式
对于一个具有 $L$ 层($L-1$ 个隐藏层,1个输出层)的网络,前向传播过程为:
$$
\begin{aligned}
z^{(1)} &= W^{(1)} x + b^{(1)}, \quad &a^{(1)} = \sigma^{(1)}(z^{(1)}) \
z^{(2)} &= W^{(2)} a^{(1)} + b^{(2)}, \quad &a^{(2)} = \sigma^{(2)}(z^{(2)}) \
&\vdots \
z^{(L)} &= W^{(L)} a^{(L-1)} + b^{(L)}, \quad &\hat{y} = g(z^{(L)})
\end{aligned}
$$
其中 $g(\cdot)$ 是输出层激活函数(如Softmax用于分类,恒等函数用于回归)。


二、 激活函数:非线性的来源

激活函数是引入非线性的关键,决定了网络的表达能力。常用激活函数如下:

函数名称 数学表达式 导数/梯度 特点与用途
Sigmoid $\sigma(z) = \frac{1}{1+e^{-z}}$ $\sigma'(z) = \sigma(z)(1-\sigma(z))$ 将输出压缩至(0,1),曾广泛使用,但两端饱和区梯度接近于零,易导致梯度消失 。
Tanh $\tanh(z) = \frac{e^{z}-e^{-z}}{e^{z}+e^{-z}}$ $\tanh'(z) = 1 - \tanh^2(z)$ 输出范围(-1,1),零中心化,收敛速度常优于Sigmoid,但仍存在梯度饱和问题 。
ReLU $\text{ReLU}(z) = \max(0, z)$ $\frac{d}{dz}\text{ReLU}(z) = \begin{cases} 1 & \text{if } z > 0 \ 0 & \text{otherwise} \end{cases}$ 最常用。计算高效,能有效缓解梯度消失。但存在“神经元死亡”问题(负半轴梯度为0)。
Leaky ReLU $\text{LeakyReLU}(z) = \begin{cases} z & \text{if } z > 0 \ \alpha z & \text{otherwise} \end{cases}$ $\frac{d}{dz}\text{LeakyReLU}(z) = \begin{cases} 1 & \text{if } z > 0 \ \alpha & \text{otherwise} \end{cases}$ ReLU的改进,为负输入提供一个小的斜率 $\alpha$(如0.01),缓解神经元死亡问题 。

三、 损失函数与输出层

损失函数 $L(y, \hat{y})$ 量化了模型预测 $\hat{y}$ 与真实标签 $y$ 之间的差距,是网络优化的目标。

1. 回归任务

  • 均方误差(MSE): $L_{\text{MSE}} = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2$
  • 输出层通常使用恒等函数(无激活)。

2. 分类任务

  • 二分类:常用二元交叉熵损失(BCE)。输出层使用 Sigmoid 函数将输出压缩到(0,1),解释为概率。
    $$
    L_{\text{BCE}} = -\frac{1}{N} \sum_{i=1}^{N} [y_i \log(\hat{y}_i) + (1-y_i) \log(1-\hat{y}_i)]
    $$
  • 多分类:常用交叉熵损失(CE)。输出层使用 Softmax 函数将输出归一化为概率分布 。
    $$
    \hat{y}k = \frac{e^{z_k}}{\sum{j=1}^{K} e^{z_j}}, \quad L_{\text{CE}} = -\frac{1}{N} \sum_{i=1}^{N} \sum_{k=1}^{K} y_{i,k} \log(\hat{y}{i,k})
    $$
    其中 $K$ 是类别数,$y
    {i,k}$ 是one-hot编码的真实标签。

四、 反向传播算法与参数优化

反向传播算法是训练MLP的核心,它基于链式法则高效计算损失函数对网络中所有参数的梯度 。

1. 算法步骤
假设网络有 $L$ 层,使用均方误差损失和Sigmoid激活(仅用于示例推导)。

  • 前向传播:计算并保存每一层的净输入 $z^{(l)}$ 和激活输出 $a^{(l)}$。
  • 计算输出层误差:$\delta^{(L)} = \frac{\partial L}{\partial z^{(L)}} = (a^{(L)} - y) \odot \sigma'(z^{(L)})$
  • 反向传播误差(对于 $l = L-1, ..., 1$):
    $$
    \delta^{(l)} = ((W^{(l+1)})^T \delta^{(l+1)}) \odot \sigma'(z^{(l)})
    $$
  • 计算梯度
    $$
    \frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T, \quad \frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}
    $$
  • 参数更新(梯度下降):
    $$
    W^{(l)} \leftarrow W^{(l)} - \eta \frac{\partial L}{\partial W^{(l)}}, \quad b^{(l)} \leftarrow b^{(l)} - \eta \frac{\partial L}{\partial b^{(l)}}
    $$
    其中 $\eta$ 是学习率 。

2. 优化器
基础的梯度下降存在收敛慢、易陷局部极小等问题。常用优化器改进如下:

优化器 核心思想 更新公式(简化版) 优点
SGD 标准梯度下降 $\theta \leftarrow \theta - \eta  
abla_{\theta} L$ 实现简单    
SGD with Momentum 引入动量项模拟惯性 $v \leftarrow \beta v + \eta  
abla_{\theta} L$
$\theta \leftarrow \theta - v$
加速收敛,减少震荡    
Adam 结合自适应学习率与动量 计算一阶矩(均值)和二阶矩(未中心化方差)的指数移动平均,并进行偏差校正后更新参数 最常用,适应不同参数,通常收敛快且稳定

五、 代码实战:实现一个简单的MLP

以下使用Python和NumPy实现一个具有一个隐藏层的MLP,用于二分类,并包含详细注释。

import numpy as np

class SimpleMLP:
    def __init__(self, input_size, hidden_size, output_size, learning_rate=0.01):
        """
        初始化一个单隐藏层MLP。
        参数:
            input_size: 输入维度
            hidden_size: 隐藏层神经元数量
            output_size: 输出维度 (二分类为1)
            learning_rate: 学习率
        """
        # 初始化权重和偏置 (使用小随机数打破对称性)
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))
        self.lr = learning_rate

    def sigmoid(self, z):
        """Sigmoid激活函数及其导数。"""
        s = 1 / (1 + np.exp(-z))
        return s

    def forward(self, X):
        """
        前向传播。
        参数:
            X: 输入数据,形状 (样本数, 输入维度)
        返回:
            y_pred: 预测概率
            cache: 保存中间变量用于反向传播
        """
        # 隐藏层线性变换
        z1 = np.dot(X, self.W1) + self.b1  # (N, hidden_size)
        # 隐藏层非线性激活
        a1 = self.sigmoid(z1)              # (N, hidden_size)
        # 输出层线性变换
        z2 = np.dot(a1, self.W2) + self.b2 # (N, output_size)
        # 输出层激活 (Sigmoid用于二分类概率输出)
        y_pred = self.sigmoid(z2)          # (N, output_size)

        cache = {'X': X, 'z1': z1, 'a1': a1, 'z2': z2, 'y_pred': y_pred}
        return y_pred, cache

    def compute_loss(self, y_pred, y_true):
        """
        计算二元交叉熵损失。
        参数:
            y_pred: 预测概率,形状 (N, 1)
            y_true: 真实标签,形状 (N, 1)
        返回:
            loss: 标量损失值
        """
        N = y_true.shape[0]
        # 添加小常数防止log(0)
        loss = -np.sum(y_true * np.log(y_pred + 1e-8) + (1 - y_true) * np.log(1 - y_pred + 1e-8)) / N
        return loss

    def backward(self, cache, y_true):
        """
        反向传播,计算梯度并更新参数。
        参数:
            cache: 前向传播保存的中间变量
            y_true: 真实标签
        """
        X, z1, a1, z2, y_pred = cache['X'], cache['z1'], cache['a1'], cache['z2'], cache['y_pred']
        N = X.shape[0]

        # 输出层梯度
        # 损失L对z2的导数: dL/dz2 = y_pred - y_true (对于Sigmoid+BCE的简化形式)
        dz2 = y_pred - y_true  # (N, output_size)
        # 损失L对W2和b2的梯度
        dW2 = np.dot(a1.T, dz2) / N  # (hidden_size, output_size)
        db2 = np.sum(dz2, axis=0, keepdims=True) / N  # (1, output_size)

        # 隐藏层梯度
        # 损失L对a1的导数: dL/da1 = dz2 * W2.T
        da1 = np.dot(dz2, self.W2.T)  # (N, hidden_size)
        # 损失L对z1的导数: dL/dz1 = dL/da1 * sigmoid'(z1)
        # sigmoid的导数: sigmoid(z)*(1-sigmoid(z))
        sigmoid_derivative_a1 = a1 * (1 - a1)
        dz1 = da1 * sigmoid_derivative_a1  # (N, hidden_size)
        # 损失L对W1和b1的梯度
        dW1 = np.dot(X.T, dz1) / N  # (input_size, hidden_size)
        db1 = np.sum(dz1, axis=0, keepdims=True) / N  # (1, hidden_size)

        # 使用梯度下降更新参数
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1

    def train(self, X_train, y_train, epochs=1000, verbose=True):
        """
        训练模型。
        参数:
            X_train: 训练数据
            y_train: 训练标签
            epochs: 训练轮数
            verbose: 是否打印训练信息
        """
        losses = []
        for epoch in range(epochs):
            # 前向传播
            y_pred, cache = self.forward(X_train)
            # 计算损失
            loss = self.compute_loss(y_pred, y_train)
            losses.append(loss)
            # 反向传播与参数更新
            self.backward(cache, y_train)

            if verbose and epoch % 100 == 0:
                print(f"Epoch {epoch}, Loss: {loss:.4f}")
        return losses

    def predict(self, X):
        """预测类别 (概率>0.5为正类)。"""
        y_prob, _ = self.forward(X)
        y_class = (y_prob > 0.5).astype(int)
        return y_class

# 示例:使用合成数据训练
if __name__ == "__main__":
    # 生成一个简单的非线性可分数据集 (同心圆)
    np.random.seed(42)
    N = 200
    # 类别0: 内圈
    radius_inner = 5
    theta_inner = np.random.rand(N//2) * 2 * np.pi
    X_inner = np.column_stack([radius_inner * np.cos(theta_inner), radius_inner * np.sin(theta_inner)]) + np.random.randn(N//2, 2)
    y_inner = np.zeros((N//2, 1))
    # 类别1: 外圈
    radius_outer = 10
    theta_outer = np.random.rand(N//2) * 2 * np.pi
    X_outer = np.column_stack([radius_outer * np.cos(theta_outer), radius_outer * np.sin(theta_outer)]) + np.random.randn(N//2, 2)
    y_outer = np.ones((N//2, 1))

    X = np.vstack([X_inner, X_outer])
    y = np.vstack([y_inner, y_outer])

    # 创建并训练模型
    mlp = SimpleMLP(input_size=2, hidden_size=10, output_size=1, learning_rate=0.1)
    losses = mlp.train(X, y, epochs=2000, verbose=True)

    # 预测并计算准确率
    y_pred = mlp.predict(X)
    accuracy = np.mean(y_pred == y)
    print(f"
Training Accuracy: {accuracy * 100:.2f}%")

六、 关键概念与难点

  1. 梯度消失/爆炸:在深层网络中,梯度在反向传播过程中可能指数级地减小(消失)或增大(爆炸),导致训练困难。使用ReLU、恰当的权重初始化(如He初始化)、批量归一化、残差连接等技巧可以缓解 。
  2. 过拟合:模型在训练集上表现过好,在测试集上泛化能力差。应对策略包括:L1/L2正则化Dropout(训练时随机丢弃部分神经元)、早停法、数据增强等。
  3. 权重初始化:不能全零初始化(导致对称性破坏失败)。常用方法有Xavier初始化(适用于Sigmoid/Tanh)和He初始化(适用于ReLU)。
  4. 批量归一化(BatchNorm):对每一层的输入进行归一化处理(减去均值,除以标准差),可以加速训练、允许使用更高的学习率、并有一定的正则化效果 。
  5. 万能近似定理:一个至少包含一个具有足够多神经元的隐藏层的前馈神经网络,可以以任意精度近似任何连续函数。这是MLP强大表达能力的理论保证 。

七、 习题

  1. 推导题:对于使用Sigmoid激活函数和交叉熵损失函数的单隐藏层MLP(二分类),请详细推导损失函数 $L$ 对隐藏层权重 $W^{(1)}$ 的梯度 $\frac{\partial L}{\partial W^{(1)}}$,明确写出链式法则的每一步。
  2. 思考题:为什么在多层感知机中,所有隐藏层都使用线性激活函数是无效的?请用数学公式证明。
  3. 编程题:扩展上面的 SimpleMLP 类,为其添加L2正则化项。修改损失函数和反向传播中的梯度计算,以包含权重衰减。
  4. 分析题:假设你在训练一个深层MLP时发现损失几乎不下降。请列出可能导致此问题的至少三种原因及相应的排查或解决方法。

参考来源

 

更多推荐