相信大家在学习神经网络的过程中,尤其是在搭建一些神经网络的过程中总是见到在Conv层后添加ReLU作为激活函数的现象,目前我也是在学习阶段,然后就自己求证了这个问题,与大家分享:
我们知道,如果不使用激活函数无论你神经网络有多少层,输出都是输入的线性组合,这相当于最原始的感知机,这种情况下网络的逼近程度就相当有限,因此我们决定引入非线性函数作为激励函数,这样深层神经网络表达能力就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数)。
选取合适的激活函数对神经网络有什么重要意义
层数比较多的神经网络模型在训练的时候会出现梯度消失和梯度爆炸问题。梯度消失问题和梯度爆炸问题一般会随着网络层数的增加变得越来越明显。梯度消失会使网络训练不动,甚至使模型的学习停滞不前。梯度爆炸一般出现在深层网络和权值初始化值太大的情况下,梯度爆炸会引起网络不稳定,最好的结果是无法从训练数据中学习,而最坏的结果是出现无法再更新的NaN权重值。
例如,一个网络含有三个隐藏层,梯度消失问题发生时,靠近输出层的hidden layer 3的权值更新相对正常,但是靠近输入层的hidden layer1的权值更新会变得很慢,导致靠近输入层的隐藏层权值几乎不变,仍接近于初始化的权值。这就导致hidden layer 1 相当于只是一个映射层,对所有的输入做了一个函数映射,这时此深度神经网络的学习就等价于只有后几层的隐藏层网络在学习。梯度爆炸的情况是:当初始的权值过大,靠近输入层的hidden layer 1的权值变化比靠近输出层的hidden layer 3的权值变化更快,就会引起梯度爆炸的问题。
梯度消失和梯度爆炸问题都是因为网络太深,网络权值更新不稳定造成的,本质上是因为梯度反向传播中的连乘效应。
ReLU函数的图像:
ReLU(Rectified Linear Unit,修正线性单元)是一种非常流行的激活函数,它在卷积神经网络(CNN)中被广泛使用,主要因为以下几个原因:
-
非线性:ReLU引入了非线性,使得网络能够学习复杂的模式和特征。如果没有非线性激活函数,无论网络有多少层,其最终都相当于一个线性模型。
-
计算简单:ReLU的计算非常快速,因为它只涉及阈值操作。对于任何输入 xx,ReLU的输出是 max(0,x)max(0,x)。
-
稀疏激活:ReLU会将负值置为0,这导致网络中的激活是稀疏的,这有助于减少计算量并可能提高网络的泛化能力。
-
缓解梯度消失问题:ReLU在正区间的斜率为1,这有助于缓解梯度消失问题,特别是在深层网络中。
此外,引入ReLU和不引入ReLU对于该神经网络来说也会有明显的差别。
-
训练速度:ReLU激活函数由于其简单性,通常可以加速训练过程,因为它在正区间的梯度恒为1,这有助于缓解梯度消失问题。
-
梯度消失/爆炸问题:在没有ReLU的网络中,如果使用Sigmoid或Tanh作为激活函数,随着网络层数的增加,梯度可能会变得非常小(梯度消失),或者变得非常大(梯度爆炸),这会导致训练困难。ReLU由于其线性特性,可以缓解梯度消失问题。
-
网络的表达能力:ReLU可以提供非线性激活,增强网络学习复杂模式的能力。没有非线性激活的网络,无论有多少层,其表达能力都相当于一个线性模型。
-
稀疏性:ReLU会导致网络中很多神经元的输出为0,这种稀疏性有助于减少计算量并可能提高网络的泛化能力。
-
死亡ReLU问题:虽然ReLU有很多优点,但它也有一个缺点,即“死亡ReLU”问题,即在训练过程中,一些神经元可能会完全“死亡”,即它们的输出始终为0。这可以通过使用变体如Leaky ReLU或Parametric ReLU来缓解。
下面是一个使用TensorFlow/Keras的示例,展示如何创建一个卷积神经网络模型,一个添加了ReLU,另一个没有添加ReLU:
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定义包含ReLU的神经网络
class NetWithReLU(nn.Module):
def __init__(self):
super(NetWithReLU, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x) # 应用ReLU激活函数
x = self.conv2(x)
return x
# 定义不包含ReLU的神经网络
class NetWithoutReLU(nn.Module):
def __init__(self):
super(NetWithoutReLU, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
# 不应用ReLU激活函数
x = self.conv2(x)
return x
# 创建包含ReLU的网络实例
net_with_relu = NetWithReLU()
# 创建不包含ReLU的网络实例
net_without_relu = NetWithoutReLU()
# 创建一个随机初始化的输入张量
input_tensor = torch.randn(1, 1, 28, 28) # batch size of 1
# 使用包含ReLU的网络进行前向传播
output_with_relu = net_with_relu(input_tensor)
print("Output with ReLU:", output_with_relu.mean().item()) # 打印输出的均值
# 使用不包含ReLU的网络进行前向传播
output_without_relu = net_without_relu(input_tensor)
print("Output without ReLU:", output_without_relu.mean().item()) # 打印输出的均值
其输出为:
输出的均值显示了ReLU激活函数对网络输出的影响。
Output with ReLU: -0.001102224923670292
表示在应用了ReLU激活函数之后,网络输出的均值接近于0,但略为负数。Output without ReLU: -0.015263878740370274
表示在没有应用ReLU激活函数的情况下,网络输出的均值更远离0,是一个更显著的负数。
这可以说明以下几点:
-
非线性:ReLU引入了非线性,使得网络可以学习更复杂的模式。即使在这个简单的例子中,ReLU的存在也对输出产生了影响。
-
稀疏性:ReLU会将所有负值置为0,这可能导致输出的激活值更加稀疏。在这个例子中,ReLU可能已经将一些负值置为0,从而影响了输出的均值。
-
数值稳定性:ReLU有助于缓解梯度消失问题,因为它在正区间的梯度恒为1。这可能有助于网络在训练过程中更快地收敛,尽管在这个简单的例子中,我们没有进行训练,但ReLU可能已经对数值稳定性产生了积极影响。
-
输出范围:ReLU将所有负值置为0,这意味着ReLU后的输出值范围是[0, ∞),而没有ReLU的输出值范围是(-∞, ∞)。这可能影响网络学习特征的能力。
-
初始化影响:由于网络权重是随机初始化的,ReLU可能会影响输出的分布,尤其是在网络的早期阶段。在这个例子中,ReLU可能帮助网络输出更接近于0的值,而没有ReLU的网络可能输出更分散的值。
-
简单示例的局限性:这个例子非常简单,只有一个前向传播过程,没有训练。在实际应用中,ReLU的效果通常在训练过程中更加明显,因为它有助于加速收敛并提高模型的泛化能力。