1. 神经网络中的激活函数:从线性到非线性的关键跃迁
在构建神经网络时,激活函数的选择往往决定了模型的表达能力。很多人刚开始接触神经网络时会有这样的疑问:既然输出层已经使用了sigmoid函数实现分类,为什么隐藏层还需要激活函数?这个问题的答案直指神经网络的核心能力——非线性建模。
1.1 为什么需要激活函数?
假设我们构建一个两层的神经网络,但隐藏层不使用任何激活函数。此时隐藏层的输出就是简单的线性变换:
$$
h = W_1x + b_1
$$
而输出层则是:
$$
\hat{y} = \sigma(W_2h + b_2) = \sigma(W_2(W_1x + b_1) + b_2)
$$
经过展开后,这实际上等价于:
$$
\hat{y} = \sigma((W_2W_1)x + (W_2b_1 + b_2))
$$
这仍然是一个线性模型!无论堆叠多少这样的"线性层",最终效果都等同于单层线性变换。这就好比用多个放大镜叠加观察物体——最终效果仍然只是一个放大镜的放大倍数。
关键理解:没有非线性激活函数,深层网络就退化为浅层线性模型,失去了"深度"的意义。
1.2 激活函数如何引入非线性?
当我们引入非线性激活函数g后,情况完全不同了:
$$
h = g(W_1x + b_1) \
\hat{y} = \sigma(W_2h + b_2)
$$
此时,网络表达的函数形式变为:
$$
\hat{y} = \sigma(W_2 \cdot g(W_1x + b_1) + b_2)
$$
由于g是非线性的,这个复合函数无法简化为单一的线性变换。以ReLU为例:
$$
ReLU(z) = max(0,z)
$$
即使是最简单的ReLU函数,也能使网络产生"折线"效果。多个ReLU单元的组合可以形成复杂的分段线性函数,理论上可以逼近任何连续函数。
1.3 常见激活函数比较
1.3.1 Sigmoid函数
数学表达式:
$$
\sigma(z) = \frac{1}{1+e^{-z}}
$$
特点:
- 输出范围(0,1),适合表示概率
- 两端饱和区梯度接近0,容易导致梯度消失
- 输出不以0为中心,可能影响收敛速度
适用场景:二分类问题的输出层
1.3.2 Tanh函数
数学表达式:
$$
tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}
$$
特点:
- 输出范围(-1,1),以0为中心
- 相比sigmoid梯度更大,训练更快
- 仍然存在梯度消失问题
适用场景:隐藏层,特别是RNN网络
1.3.3 ReLU函数
数学表达式:
$$
ReLU(z) = max(0,z)
$$
特点:
- 计算简单,梯度要么是0要么是1
- 缓解梯度消失问题
- 可能导致"神经元死亡"
- 输出不以0为中心
适用场景:CNN和大多数前馈网络的隐藏层
1.3.4 Leaky ReLU
数学表达式:
$$
LReLU(z) = \begin{cases}
z, & z > 0 \
\alpha z, & z \leq 0
\end{cases}
$$
特点:
- 解决了ReLU的"死亡神经元"问题
- 需要调参选择α值(通常0.01)
- 计算仍然高效
适用场景:当遇到ReLU导致大量神经元死亡时
1.4 激活函数选择实践建议
- 隐藏层首选ReLU:简单高效,适合大多数情况
- 输出层根据任务选择:
- 二分类:Sigmoid
- 多分类:Softmax
- 回归:线性或ReLU(非负输出)
- 遇到训练问题时尝试:
- 梯度消失:Leaky ReLU或Swish
- 输出范围限制:Tanh
- 特殊架构要求:
- RNN:Tanh或Sigmoid
- 残差网络:ReLU
经验法则:从ReLU开始,遇到问题再尝试其他激活函数。不要过早优化。
2. 浅层神经网络的反向传播机制
理解了激活函数的作用后,我们需要掌握如何训练这样的网络。反向传播算法是神经网络训练的核心,对于浅层网络,它已经展现出深度学习中的关键概念。
2.1 前向传播过程
考虑一个具有单隐藏层的网络:
- 输入层到隐藏层:
$$
Z^{[1]} = W^{[1]}X + b^{[1]} \
A^{[1]} = g^{[1]}(Z^{[1]})
$$ - 隐藏层到输出层:
$$
Z^{[2]} = W^{[2]}A^{[1]} + b^{[2]} \
A^{[2]} = \sigma(Z^{[2]})
$$
其中:
- 上标[1],[2]表示层号
- X是输入矩阵(n×m,n是特征数,m是样本数)
- g是隐藏层激活函数
- σ是输出层sigmoid函数
2.2 损失函数
使用二分类交叉熵损失:
$$
J = -\frac{1}{m}\sum_{i=1}^m [y^{(i)}\log(a^{2}) + (1-y^{(i)})\log(1-a^{2})]
$$
向量化形式:
$$
J = -\frac{1}{m} [Y^T \log A^{[2]} + (1-Y)^T \log(1-A^{[2]})]
$$
2.3 反向传播推导
2.3.1 输出层梯度
首先计算损失对输出层线性组合的导数:
$$
\frac{\partial J}{\partial Z^{[2]}} = A^{[2]} - Y = dZ^{[2]}
$$
然后计算权重和偏置的梯度:
$$
dW^{[2]} = \frac{1}{m} dZ^{[2]} A^{[1]T} \
db^{[2]} = \frac{1}{m} \sum dZ^{[2]}
$$
2.3.2 隐藏层梯度
误差传播到隐藏层:
$$
dA^{[1]} = W^{[2]T} dZ^{[2]}
$$
考虑激活函数的导数:
$$
dZ^{[1]} = dA^{[1]} \odot g^{[1]\prime}(Z^{[1]})
$$
最后计算隐藏层参数梯度:
$$
dW^{[1]} = \frac{1}{m} dZ^{[1]} X^T \
db^{[1]} = \frac{1}{m} \sum dZ^{[1]}
$$
2.4 参数更新
使用梯度下降更新参数:
$$
W^{[1]} := W^{[1]} - \alpha dW^{[1]} \
b^{[1]} := b^{[1]} - \alpha db^{[1]} \
W^{[2]} := W^{[2]} - \alpha dW^{[2]} \
b^{[2]} := b^{[2]} - \alpha db^{[2]}
$$
其中α是学习率。
2.5 维度检查技巧
在实际实现中,维度匹配是常见错误源。记住这个检查表:
| 变量 | 维度 |
|---|---|
| W[1] | (hidden,n) |
| b[1] | (hidden,1) |
| Z[1],A[1] | (hidden,m) |
| W[2] | (1,hidden) |
| b[2] | (1,1) |
| Z[2],A[2] | (1,m) |
| dZ[2] | (1,m) |
| dW[2] | (1,hidden) |
| db[2] | (1,1) |
| dA[1] | (hidden,m) |
| dZ[1] | (hidden,m) |
| dW[1] | (hidden,n) |
| db[1] | (hidden,1) |
3. 实现细节与常见问题
3.1 初始化技巧
参数初始化对训练成功至关重要:
- 权重:使用He初始化(ReLU)或Xavier初始化(Tanh)
python复制# ReLU激活的He初始化 W = np.random.randn(layer_size[1], layer_size[0]) * np.sqrt(2./layer_size[0]) - 偏置:初始化为0是常见做法
3.2 学习率选择
学习率α的影响:
- 太大:震荡或发散
- 太小:收敛缓慢
实践建议:
- 从0.01开始尝试
- 使用学习率衰减策略
- 考虑自适应优化器(Adam)
3.3 梯度检查
实现反向传播时,数值梯度检查是验证正确性的好方法:
python复制def gradient_check(parameters, gradients, X, Y, epsilon=1e-7):
# 将参数展平
parameters_values = dictionary_to_vector(parameters)
grad = gradients_to_vector(gradients)
num_parameters = parameters_values.shape[0]
J_plus = np.zeros((num_parameters, 1))
J_minus = np.zeros((num_parameters, 1))
gradapprox = np.zeros((num_parameters, 1))
for i in range(num_parameters):
thetaplus = np.copy(parameters_values)
thetaplus[i][0] += epsilon
J_plus[i] = forward_propagation_n(X, Y, vector_to_dictionary(thetaplus))
thetaminus = np.copy(parameters_values)
thetaminus[i][0] -= epsilon
J_minus[i] = forward_propagation_n(X, Y, vector_to_dictionary(thetaminus))
gradapprox[i] = (J_plus[i] - J_minus[i]) / (2*epsilon)
numerator = np.linalg.norm(grad - gradapprox)
denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox)
difference = numerator / denominator
if difference > 2e-7:
print("梯度检查失败!差异太大:" + str(difference))
else:
print("梯度检查通过!差异:" + str(difference))
return difference
3.4 常见问题排查
-
损失不下降:
- 检查梯度计算是否正确
- 尝试更小的学习率
- 检查初始化是否合理
-
输出全是0或1:
- 可能是梯度消失
- 尝试Leaky ReLU或调整初始化
-
训练集表现好但测试集差:
- 可能过拟合
- 增加正则化(L2/dropout)
- 获取更多数据
-
NaN值出现:
- 检查数值稳定性
- 添加梯度裁剪
- 检查学习率是否过大
4. 扩展思考与进阶方向
4.1 深层网络的挑战
浅层网络到深层网络的转变带来新挑战:
- 梯度消失/爆炸问题更严重
- 参数初始化更加关键
- 计算成本显著增加
- 需要更复杂的正则化策略
4.2 现代激活函数发展
近年来出现的新激活函数:
- Swish:β·x·sigmoid(βx),表现优于ReLU
- GELU:高斯误差线性单元,用于Transformer
- SELU:自归一化网络的专用激活函数
4.3 自动微分框架的优势
现代深度学习框架(TensorFlow,PyTorch)提供:
- 自动微分,无需手动推导
- GPU加速支持
- 丰富的预构建层和优化器
虽然理解底层原理很重要,但在实际项目中,使用这些框架可以大幅提高效率。
4.4 从理论到实践的过渡建议
- 先在小数据集上实现并调试
- 可视化激活和梯度分布
- 使用成熟的框架验证结果
- 逐步扩展到更复杂架构
在实践中,我发现将理论推导转化为实际代码时,维度匹配和梯度检查是最容易出错的两个环节。建议在实现时先固定随机种子,使用小批量数据,确保每一步的维度都符合预期。当网络无法学习时,从最简单的案例开始(如学习AND/OR逻辑),逐步增加复杂度。
