共计 2538 个字符,预计需要花费 7 分钟才能阅读完成。
从单层感知机到多层神经网络
初学机器学习时,我们常从单层感知机(Perceptron)开始。这种简单模型能解决线性可分问题,比如基本的 AND/OR 逻辑运算。但遇到非线性问题(如异或 XOR)时,单层结构就暴露了致命缺陷——它无法学习非线性关系。这就引出了多层神经网络的必要性:通过叠加隐藏层和非线性激活函数,网络可以拟合任意复杂函数。

核心原理三步走
1. 前向传播的矩阵化表示
前向传播是数据从输入层流向输出层的过程。假设我们有一个单隐藏层网络:
- 输入层维度:$n_{input}$
- 隐藏层维度:$n_{hidden}$
- 输出层维度:$n_{output}$
权重矩阵和偏置的维度分别为:
$$
W_1 \in \mathbb{R}^{n_{input} \times n_{hidden}}, \quad
b_1 \in \mathbb{R}^{n_{hidden}}
$$
$$
W_2 \in \mathbb{R}^{n_{hidden} \times n_{output}}, \quad
b_2 \in \mathbb{R}^{n_{output}}
$$
前向传播公式:
$$
\begin{aligned}
z_1 &= X W_1 + b_1 \
a_1 &= \sigma(z_1) \
z_2 &= a_1 W_2 + b_2 \
\hat{y} &= \sigma(z_2)
\end{aligned}
$$
其中 $\sigma$ 是 Sigmoid 激活函数。矩阵化实现让计算更高效,尤其适合 GPU 并行。
2. 损失函数与反向传播
采用均方误差损失函数:
$$
J = \frac{1}{2m}\sum_{i=1}^m (\hat{y}_i – y_i)^2
$$
关键是通过链式法则计算梯度。以输出层权重 $W_2$ 为例:
$$
\frac{\partial J}{\partial W_2} =
\underbrace{\frac{\partial J}{\partial \hat{y}}}{\delta_2} \cdot
\underbrace{\frac{\partial \hat{y}}{\partial z_2}} \cdot
\underbrace{\frac{\partial z_2}{\partial W_2}}_{a_1}
$$
实际实现时,我们会先计算输出层误差 $\delta_2$,再反向传播到隐藏层:
$$
\delta_1 = (\delta_2 W_2^T) \odot \sigma'(z_1)
$$
3. 学习率与批量大小
- 学习率 :太大导致震荡,太小收敛慢。建议从 0.01 开始尝试
- 批量大小 :小批量(如 32/64)比全批量更高效,兼具随机性和稳定性
二者需要协同调整:增大批量时,可适当增加学习率。经验公式:
$$
\text{新学习率} = \text{原学习率} \times \frac{\text{ 新批量大小}}{\text{ 原批量大小}}
$$
Python 实战代码
Sigmoid 及其导数实现
import numpy as np
def sigmoid(x):
"""向量化的 Sigmoid 函数"""
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
"""Sigmoid 的导数,输入可以是原始值或激活值"""
s = sigmoid(x) if np.abs(x).max() > 10 else x # 避免重复计算
return s * (1 - s)
单隐藏层网络训练
# 数据标准化(重要!)X = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
# 初始化参数(Xavier 方法)W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1/input_size)
b1 = np.zeros(hidden_size)
W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1/hidden_size)
b2 = np.zeros(output_size)
for epoch in range(epochs):
# Mini-Batch 划分
for i in range(0, len(X), batch_size):
X_batch = X[i:i+batch_size]
y_batch = y[i:i+batch_size]
# 前向传播
z1 = np.dot(X_batch, W1) + b1
a1 = sigmoid(z1)
z2 = np.dot(a1, W2) + b2
y_pred = sigmoid(z2)
# 反向传播
delta2 = (y_pred - y_batch) * sigmoid_derivative(z2)
dW2 = np.dot(a1.T, delta2) / batch_size
db2 = np.sum(delta2, axis=0) / batch_size
delta1 = np.dot(delta2, W2.T) * sigmoid_derivative(z1)
dW1 = np.dot(X_batch.T, delta1) / batch_size
db1 = np.sum(delta1, axis=0) / batch_size
# 参数更新
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
避坑指南
梯度消失问题
当使用 Sigmoid 激活时,深层网络容易出现梯度消失。解决方案:
- 改用 ReLU 激活函数:$\text{ReLU}(x) = \max(0, x)$
- 残差连接(ResNet 思路)
权重初始化
避免全零初始化!推荐方法:
- Xavier 初始化:$W \sim N(0, \sqrt{\frac{2}{n_{in}+n_{out}}})$
- He 初始化(ReLU 专用):$W \sim N(0, \sqrt{\frac{2}{n_{in}}})$
延伸思考
- 如何用 TensorBoard 可视化训练过程?
- 记录损失、准确率、权重分布等
-
可视化计算图和高维嵌入
-
Adam vs SGD:
- Adam 适合非平稳目标,自动调整学习率
- SGD 更稳定,可能找到更优解但需精细调参
通过这次实践,我深刻体会到:理论推导是基础,但只有亲手实现代码,才能真正掌握神经网络的精妙之处。建议读者尝试修改网络结构,观察不同超参数对训练的影响,这是提升理解的最佳途径。
