正则化与参数初始化对神经网络的影响

正则化

引入

深度学习最常见的两个问题——过拟合与欠拟合。
在深度学习中,解决问题需要三步:

  1. 定义一个模型
  2. 通过定义一个损失函数判断模型的好坏
  3. 选取一个最好的模型

我们选取的每一个模型都可以看做是对数据的一种拟合,模型的位置与真实值之间的差距称为偏移bias,偏移量是衡量一个模型是否有用的重要标准。所以bias偏移量越大,模型越差。一般来说,高阶的模型偏移较小,更贴近真实数据的分布,但是当我们用新的测试集在高阶模型上进行预测时,由于高阶模型的形态过分贴合训练集数据且较为复杂,它在新的测试集数据上的拟合优度一定不会很好,这就形成了过拟合。与之相反,如果一个模型发生了欠拟合,那意味着这个模型在根本层面上就很难很好地反映出数据的特征,有可能会直接放弃这个模型。

模型的选择需要我们的决策,通常是去选择一个高阶模型,因为欠拟合一旦发生是无法改善的,只能重新定义模型,而高阶模型具备对数据的拟合能力,至于过拟合问题可以通过其他手段优化的。

在统计学中,过拟合(英语:overfitting,或称拟合过度)是指过于紧密或精确地匹配特定数据集,以致于无法良好地拟合其他数据或预测未来的观察结果的现象。过拟合模型指的是相较有限的数据而言,参数过多或者结构过于复杂的统计模型。发生过拟合时,模型的偏差小而方差大。过拟合的本质是训练算法从统计噪声中不自觉获取了信息并表达在了模型结构的参数当中。相较用于训练的数据总量来说,一个模型只要结构足够复杂或参数足够多,就总是可以完美地适应数据的。过拟合一般可以视为违反奥卡姆剃刀原则。

简单来说,过拟合问题就是定义的模型过于复杂或者数据太少,使得神经网络在训练时学习了不该学习的东西,如环境噪声、标记错误的数据等,这些会让网络模型在测试集上的表现不那么准确,即模型的泛化能力弱。

一般情况下,过拟合现象表现为模型在训练集(原始数据集)上误差非常低,在一个全新的测试集(全新数据集)上,模型的误差又非常大。

针对深度学习可能存在过拟合问题,有两个解决方法,一个是正则化,另一个是准备更多的数据,但有时可能无法准备足够多的训练数据或者获取更多数据的成本很高,而正则化有助于避免过度拟合,或者减少网络误差,这就成为我们最常使用的一个方法。

L2正则化

正则化的目的

加入正则化的目的就是让模型的参数变小,使模型对数据敏感度下降(模型参数大小决定了当数据发生变化时输出结果的变化程度),所以正则项的加入就是降低模型变化率的过程,变化率降低直观表现为模型更加平滑,使模型更加集中且增大了与真实模型之间的距离,也就是增大了偏移量bias,从而增强了高阶过拟合模型的泛化能力。实际使用时,最常用的是L1和L2正则化
正则化公式
L1、L2正则

L1正则和L2正则的区别,在这个视角下去理解就会变得非常直观。因为L1正则是参数的一次项直接相加,在图像上表现出来的结果就是让原来的拟合模型从“繁杂、曲折”变得“平滑但依然有曲折点”,换句话说就是不同段的数据特征依然保留着强烈的不同特点。而L2正则由于是参数的平方项相加,会让原拟合模型的阶段性特点不过于明显,而具有更强的平滑性(从圆和正方形的形态可以看出)。更为详细的内容可以参考以下两篇文章

扫描二维码关注公众号,回复: 14411979 查看本文章
  1. 如何通俗易懂地解释「范数」?
  2. 机器学习中正则化项L1和L2的直观理解

为什么正则化有效?

通过吴恩达课程中的一个例子来说明为什么正则化对过拟合有效。

假设使用双曲线激活函数tanh,使用L2正则化
tanh
令输出 g ( z ) = t a n h ( z ) g(z)=tanh(z) g(z)=tanh(z),就会发现,当z很小或者z值涉及少量参数时,双曲正切函数就会呈现一个线性状态(如下图1标注),当z值更大或更小时,tanh才开始变得非线性(如下图标注2和3)。
线性状态
当正则化参数 λ \lambda λ很大时,参数 w w w会变小,(为什么 w w w会变小?——原因在我的前一篇文章反向传播的计算过程的最后部分有推导),而对每一层激活函数的输入z,都有
z [ l ] = w [ l ] a [ l − 1 ] + b [ l ] z^{[l]}=w^{[l]}a^{[l-1]}+b^{[l]} z[l]=w[l]a[l1]+b[l]
所以当 w w w很小的时候,z也会变小,如果最终z的范围在上图标注1的范围内,则 g ( z ) g(z) g(z)会呈现出线性,导致几乎每一层都是线性的,如果每层都是线性的,那么整个网络就是一个线性网络。这样,即使是一个非常深的深层网络,因具有线性激活函数的特征,最终只能计算线性函数,因此,它不适用于非常复杂的决策以及过度拟合数据集的非线性决策边界。

总结一下,如果正则化参数 λ \lambda λ很大,那么 w w w z z z也就相应变小,当 z z z的取值恰好处于某段区间时,tanh会相对呈线性,整个神经网络会计算离线性函数近的值,这个线性函数非常简单,并不是一个极复杂的高度非线性函数,不会发生过拟合,这就是正则化对处理过拟合问题有效的原因。

关于L2正则化对网络的影响,这里不再给出,将在后续的MNIST训练实验中详细展示。

Dropout正则化

前面提到过,过拟合现象的出现很多时候是因为模型过于复杂,Dropout在网络的不同层以给定概率随机丢弃一些节点,这样可以使网络的规模减小,需要注意的是,Dropout仅在训练时使用,在测试集上运行时需要关闭,这一点在pytorch中已经有了很好的封装,通过调用model.train()和model.eval()在训练和测试模式之间切换,model.eval()可以使网络在测试时不使用Batch Normalization 和 Dropout。

对于每层网络的不同神经元(节点),我们给定的权重都是不同的,在Dropout中,不会依赖于任何一个特征,因为这个单元可能会被丢弃,在随机选择丢弃节点后,Dropout会对输入权重做出调整,在标准dropout正则化中,通过按保留(未丢弃)的节点的分数进⾏归⼀化来消除每⼀层的偏差。每个中间激活值h以丢弃概率p由随机变量h′替换,如下所示:
在这里插入图片描述
通过这种方式,保持期望值不变,即 E [ h ′ ] = p ∗ 0 + ( 1 − p ) ∗ h 1 − p = h E[h′] =p*0+(1-p) * \frac{h}{1-p} =h E[h]=p0+(1p)1ph=h

因此,我们不会对一个节点的输入给定太多的权重,而是为每个输入增加权重,这种调节权重的方式类似于L2正则化,区别在于L2正则化对不同权重的衰减是不同的。

Dropout作用
在上面这张图片中,每一层的输入维度不同,所需权重矩阵的大小也有不同,可以看出矩阵 w [ 2 ] w^{[2]} w[2]的规模最大,为7×7,在设定Dropout丢弃概率时可以偏大一些,而对于较小规模的权重矩阵,概率要相对低一些,这些都由我们定义网络结构时根据具体情况调节。

在pytorch中,可以使用nn.Dropout()和nn.functional.dropout()定义Dropout层,给定参数p表示被丢弃的概率,需要注意的一点是nn.functional.dropout()的参数training默认是False,所以我们在调用F.dropout()时需要更改training=True,否则无法使用dropout。

下面用一个小的示例来观察Dropout正则化的效果。

为了显示过拟合效果,这里自定义数据集,缩小数据规模,给定50个训练数据,并且将网络层定义200个计算单元

# training set
x = torch.unsqueeze(torch.linspace(-1, 1, 50), dim=1) 
y = x + 0.3*torch.normal(torch.zeros(50, 1), torch.ones(50, 1))
x, y = Variable(x), Variable(y) 

# test set
test_x = torch.unsqueeze(torch.linspace(-1, 1, 50), dim=1)
test_y = test_x + 0.3*torch.normal(torch.zeros(50,1), torch.ones(50,1))
test_x, test_y = Variable(test_x), Variable(test_y)

定义两个网络,一个作为没有dropout层的过拟合,另一个添加dropout层

hidden_num = 200

# 过拟合模型
model_overfitting = nn.Sequential(
    nn.Linear(1, hidden_num), 
    nn.ReLU(),                
    nn.Linear(hidden_num, hidden_num), 
    nn.ReLU(), 
    nn.Linear(hidden_num, 1)
)

# 有Dropout的模型
model_dropout = nn.Sequential(
    nn.Linear(1,hidden_num),
    nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(hidden_num,hidden_num),
    nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.ReLU(),
    nn.Linear(hidden_num,1)
)

然后开始训练

optimizer_overfitting = optim.Adam(model_overfitting.parameters(), lr=0.01)
optimizer_dropout = optim.Adam(model_dropout.parameters(), lr=0.01)
loss = nn.MSELoss()
loss_overfitting_history = []
loss_dropout_history = []

for epoch in range(100):
    pred_overfitting = model_overfitting(x)
    pred_dropout = model_dropout(x)
    loss_overfitting = loss(pred_overfitting, y)
    loss_dropout = loss(pred_dropout, y)

    loss_overfitting_history.append(loss_overfitting.item())
    loss_dropout_history.append(loss_dropout.item())
    
    optimizer_overfitting.zero_grad()
    optimizer_dropout.zero_grad()
    loss_overfitting.backward()
    loss_dropout.backward()
    optimizer_overfitting.step()
    optimizer_dropout.step()

用自定义的test数据集测试,并展示结果

# test set测试
model_dropout.eval()

test_pred_overfitting = model_overfitting(test_x)
test_pred_dropout = model_dropout(test_y)

print('model-overfitting test loss:',loss(test_pred_overfitting, test_y).item())
print('model-fropout test loss:',loss(test_pred_dropout, test_y).item())

plt.title('training loss')
plt.plot(range(1,101), loss_overfitting_history,'r--',label='train-loss-overfitting')
plt.plot(range(1,101), loss_dropout_history,'b--',label='train-loss-dropout')

plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()

结果如下
dropout对比结果
从图片中可以看出两个模型训练效果基本相同,但是将训练完成的模型在测试集上运行,再给出测试集的loss值,发现添加dropout模型的测试loss要低得多,而没有dropout的overfitting模型效果就不是这么好,这也说明添加dropout层可以改善过拟合现象。

数据增强

数据增强通过扩增训练数据来解决过拟合问题,但扩增数据代价高,在处理图像问题时可以变换原有的图片数据集实现数据增强。pytorch在torchvision.transforms模块中封装了许多图像变换的操作,例如以一定概率翻转图片、裁剪图片等,这些都可以利用原图片扩大数据规模,在一定程度上减少了过拟合问题,所以数据增强也可以看做是一种正则化的方式。

在我的前一篇博客CNN网络的搭建(Lenet5与ResNet18)中,使用ResNet18对CIFAR-10数据集分类,在初始化训练数据集时定义了两种数据增强方式,分别是随机裁剪和水平翻转。

这里将两种操作去除,为了节省时间仅训练30个epoch做对比

epoch 5 , loss: 0.6901260018348694
epoch 5 , test acc: 67.78 %
--------------------------------------
epoch 10 , loss: 0.2946148216724396
epoch 10 , test acc: 68.49 %
--------------------------------------
epoch 15 , loss: 0.10665436089038849
epoch 15 , test acc: 68.19 %
--------------------------------------
epoch 20 , loss: 0.019109390676021576
epoch 20 , test acc: 69.75 %
--------------------------------------
epoch 25 , loss: 0.11086629331111908
epoch 25 , test acc: 68.67999999999999 %
--------------------------------------
epoch 30 , loss: 0.028161248192191124
epoch 30 , test acc: 68.55 %
--------------------------------------

无数据增强
可以看到,训练集上的loss不断下降,但是测试集上的准确率停滞不前,这很可能出现了过拟合,对比之前加上数据增强操作的训练结果
数据增强结果
能够明显看出,在前30个epoch中,无论是training loss还是test accuracy,效果都在变好,30个epoch后,测试集准确率约为80%,数据增强对于过拟合问题有一定的作用。

参数初始化

pytorch中的几种参数初始化方法可参考这篇博文:pytorch中的参数初始化方法总结

下面给出一个简单的示例来比较不同参数初始化方法对训练loss值的影响,不再对测试集的准确率做测试。

还是使用MNIST数据集,按照下面这个模型手动编写代码
模型

标准正态分布初始化

模型实现如下,首先使用标准正态分布生成参数 w i , b i w_i,b_i wi,bi

epoches = 5
batch_size = 200
learning_rate = 0.01

w1, b1 = torch.randn(200, 784, requires_grad=True), torch.zeros(200, requires_grad=True)
w2, b2 = torch.randn(200, 200, requires_grad=True), torch.zeros(200, requires_grad=True)
w3, b3 = torch.randn(10, 200, requires_grad=True), torch.zeros(10, requires_grad=True)

def forward(x):
	# layer1
    x = x @ w1.t() + b1
    x = F.relu(x)
    # layer2
    x = x @ w2.t() + b2
    x = F.relu(x)
    # layer3 (Output)
    x = x @ w3.t() + b3
    x = F.relu(x)
    return x

optimizer = optim.SGD([w1,b1,w2,b2,w3,b3],lr=learning_rate)
loss = nn.CrossEntropyLoss()

loss_history = []

for epoch in range(epoches):
    for batch_idx,(data,target) in enumerate(train_loader):
        data = data.view(-1,28*28)
        logits = forward(data)
        l = loss(logits,target)
        optimizer.zero_grad()
        l.backward()
        optimizer.step()
        if batch_idx % 100 == 0:
            loss_history.append(l.item())
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), l.item()))

plt.plot(range(len(loss_history)), loss_history)
plt.ylim(0, 3)
plt.ylabel('Loss')
plt.show()

运行之后发现第一次的loss值过大,影响对后面loss变化趋势的查看,而后序的loss值一直在2.3之间浮动,因此将y轴值限制在2~3之间,可以更清晰地看出训练集上的损失值变化趋势,如下图

标准正态分布

kaiming初始化

接下来使用kaiming正态分布初始化网络参数

torch.nn.init.kaiming_normal_(w1)
torch.nn.init.kaiming_normal_(w2)
torch.nn.init.kaiming_normal_(w3)

训练所得loss变化如下:
kaiming初始化

实验总结

从上面的例子可以看出,参数初始化对模型训练结果有着很大的影响,使用kaiming初始化方法的训练损失经过几个epoch的训练后,loss可以降低至0.5以下,而使用标准正态分布初始化的模型loss一直在2.3附近浮动,并且基本不再变化。

下面查看torch.nn中给定模型的参数初始化方式,例如nn.Linear(),nn.Conv2d()等,跟踪进入源码可以看到它们的参数初始化方式

  1. nn.Linear()
    Linear初始化方式
    可以看到权值参数 w i w_i wi是通过kaiming_uniform_(kaiming均匀分布)初始化的,而偏移bias通过默认的从-bound到bound的均匀分布来初始化的。
  2. nn.Conv2d()
    conv2d
    nn.Conv2d()继承自_ConvNd,查看_ConvNd的定义_ConvNd
    同样可以看到pytorch给定它的参数初始化方式,与Linear相同。

当想要更改pytorch默认的参数初始化方法时,可以选择pytorch中Module类提供的apply()方法,并且官方给出了可供参考的一个例子

    def apply(self: T, fn: Callable[['Module'], None]) -> T:
        for module in self.children():
            module.apply(fn)
        fn(self)
        return self

apply方法递归地遍历模型的所有孩子节点(子模型…)并按照设定的初始化方法进行参数初始化。

使用方法如下:

def init_weights(m):
	print(m)
    if type(m) == nn.Linear:
    	m.weight.fill_(1.0)
    	print(m.weight)

net = nn.Sequential(nn.Linear(2, 2), nn.Linear(2, 2))
net.apply(init_weights)

定义init_weights方法,可对不同的Module模型进行不同的参数初始化,最后使用定义好的模型调用apply方法即可。

附加解释

使用cs231n课程的例子解释,用不同的方式初始化一个十层的神经网络的权值参数,每层使用tanh激活函数

  1. 使用标准差为1的高斯分布初始化权值,即
    w = np.random.randn(node_num, node_num) * 1
    
    最后得到各层激活值的分布如下:
    标准差为1
    数据集中分布偏向于-1、1两个值,根据tanh的特点,数据偏向-1或1时,会有梯度饱和的问题,造成梯度更新缓慢或者无法更新的问题出现。
  2. 使用标准差为0.01的高斯分布初始化权值,即
    w = np.random.randn(node_num, node_num) * 0.01
    
    此时激活值的分布为
    标准差0.01
    这时数据集中在0附近,由于tanh激活函数是以零为中心的,所以这种分布的数据会使得梯度在传播过程中逐渐减小并趋近与0,产生梯度消失问题。

这就要求各层激活值的分布要有一定的广度,在各层之间传递多样性的数据,如果传递的值有所偏向,就会出现梯度消失或者表现力受限,影响学习的进行。

  1. Xavier初始化:若前一层节点数为n,则初始值使用标准差为 1 n \frac{1}{\sqrt n} n 1的高斯分布,即
    w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
    
    这时激活值分布为:
    Xavier初始化
    数据呈现出较好的广度,避免了梯度消失和激活函数表现力受限的问题。

因此,在神经网络的学习中,权值初始化非常重要,但也是容易被忽视的一点,通过两个实验说明了其重要性。

猜你喜欢

转载自blog.csdn.net/qq_41533576/article/details/119296182