1.常见的分类问题
学完了多层感知机,知道什么是前向传播、反向传播,其实就已经在开始窥探深度学习的精髓了。整个深度学习,都是在此基础上发展的,多层感知机更加复杂的版本,并没有离开此区域。
往后学习 的而原因是,实际的情况千奇百怪,十分复杂,不是简单的多层感知机就能解决好的,因此需要明确要解决的问题是什么。
1.1模型架构设计
架构设计包含网络深度、神经元个数、层类型、连接方式。
架构设计(architecture design),指的是在解决特定的实际问题时,选择合适的网络结构。
节点的数量也就是神经元的数量;网络层数、不同类型的层,到底是使用全连接层还是卷积层、循环层,或者注意力机制等不同类型的层。以及在层与层之间,如何连接。
1.2万能近似定理
Universal Approximation Therem(Cybenko,1989):一个具有足够多的隐藏节点的多层前馈神经网络,可以逼近任意连续的函数。它需要包含至少一种有挤压性质的激活函数,比如说Sigmoid、Tanh、ReLU等,能够进行非线性的空间变换。这种情况下,即使是非常复杂的函数,哪怕只有一个隐藏层的神经网络,可以通过不断地调整权重(参数)来近似,这是理论上可行,它有可能需要非常非常大的隐藏层。
在实际问题中,需要综合考虑训练数据的数量、训练的时间和过拟合问题,不可能去选择一个无限大的单层的神经网络来解决,尽管理论上是可行的。
1.3宽度or深度
作为初学者,特别想知道,设计神经网络时,到底应该选多少层,每一层有多少个神经元呢。这个问题并没有固定的答案,很多时候,取决于问题或者说是正在研究问题的类型。更大的隐藏层,在理论上(前面提到的万能近似定理),它可以提供更多的参数学习数据表示,但是也有可能产生过拟合。
更深的网络,可以通过更多的层来学习抽象的特征。但是也可能会受到梯度消失的影响。对于特定的问题,需要通过调整模型的大小来找到最佳的模型效果。不过,已经有一些初步的研究证明国内外的研究学者,通过实验而不是理论上(数学上还没有证明),通过这些实验发现,很多情况下,增加网络的深度,相对于增加宽度而言,更有助于提高模型的泛化能力。
泛化能力是指,训练后的模型,拿新的数据进行测试,准确率依然很高,不会产生过拟合问题。选择这种深度模型或者说是理念,默许了一个看法,就是想要学得的函数,往往会涉及几个更加简单的函数的组合,换句话说保持一定宽度的情况下,增加网络的深度,有助于去捕获一些新的数据特征,因而有助于提高模型的泛化能力。
多少层算深呢?对cdn图像任务来说,十几层已经算是深了,比较流行的ResNet这种几百上千层也是经常的。
1.4过拟合问题
过拟合(Overfitting):模型在训练数据上表现良好,在测试数据上表现不佳。意味着模型对训练数据进行了过度的拟合,从而无法适用于真实世界中的数据。
泛化能力:训练后的模型应用到新的、未知的数据上的能力;
产生原因:通常是由于模型复杂度过高导致的。
模型更复杂时,好处是可以更好地拟合训练数据中的噪声和细节,但是也会使得模型对真实世界的数据产生过度地拟合。
1.5欠拟合问题
欠拟合(Underfitting):学习能力不足,无法学习到数据集中的“一般规律”;
产生原理:模型学习能力较弱,或数据复杂度较高地情况。
为了避免欠拟合,可以通过增加模型的复杂度来提高模型的表示能力,包括增加网络的层数或者神经元的数量;使用更加复杂的模型结构,比如说卷积神经网络或者循环神经网络。
1.6相互关系
横轴模型复杂度,与泛化误差、训练误差存在一定的依存关系。纵轴误差,随着模型复杂度的增加(表征能力变强),训练误差会越来越小,但是泛化误差不一定。
训练完的模型用新的数据进行测试, 结果不一定好,模型复杂度变高,训练误差降低,泛化误差反而增加,这就产生了过拟合现象。当模型复杂度很低时,会产生欠拟合现象,处于中间是最佳的情况。在实际应用时,希望泛化误差尽可能小,同时避免过拟合。
2.过拟合欠拟合应对策略
2.1问题的本源
问题本源包含数据和模型匹配问题、数据复杂度、模型复杂度、训练策略。
过拟合欠拟合两种问题,本质上都是数据和模型匹配出现了问题,因此解决的颁发可以从数据的复杂度、模型的复杂度、训练策略三个方面进行。
2.2数据集大小的选择
数据集较小,很容易出现过拟合(因为数据集小,模型很容易就拟合数据);数据过大可能导致训练效率降低。在选择数据集大小的时候需要权衡利弊,选择合适大小的数据集。
2.3数据增广
既然提升模型泛化能力或者说是解决过拟合问题的一个好颁发是使用更多的数据进行训练,实际情况数据量非常有限,解决这个问题就是创建假的数据,添加到数据集中,这就是数据增强技术(Data Augment)。使用数据增光,可以有效解决过拟合问题,提高模型在新数据上的泛化能力。
对训练数据进行变换(旋转、翻转、剪切、加入噪声、改变亮度),增加数据数量和多样性。
2.4使用验证集
验证集(validation set):训练中评估模型性能,调整超参数。
把数据集除了分为训练集和测试机之外,还可以分出来一块叫做验证集,在训练中使用,不是用于训练数据,通过验证集去评估,选超参数,主要用于评估模型的性能,用于比较不同模型(不同超参数)的性能,选择最优的模型。选择超参数的好处是可以在训练过程中,就对模型就行评价,而不需要等到训练结束后,这样可以及时发现模型问题,及时调整模型超参数。
2.5模型选择
过于简单的模型会带来欠拟合,解决这个问题就是让模型更复杂点(隐藏层、神经元多一些)。但是对于过于复杂的模型,又会带来过拟合。
目前公认的深度学习的规律就是deeper is better,就是越深越好。国内外研究者,通过实验和竞赛发现,像CNN卷积神经网络模型层数越多,效果越好。 理论上不知道原因。实物有两面性,神经网络层数越多,越容易出现过拟合,在模型选择或模型设计上,最好的**原则就是奥卡姆剃刀法则:择尽量简单、合适的模型来解决复杂的问题。**在同样能够解释已知的数据或观测现象的模型中,挑选最简单的。
2.6K折交叉验证
对训练的过程进行改进来解决过拟合问题,K折交叉验证就是其中的一个,步骤如下:
1.将训练数据分成K个不相交的子集;
2.对于每份数据作为验证集,剩余的K-1份数据作为训练集,进行训练和验证;
3.计算K次验证的平均值。
下图是4折交叉验证流程图,将数据集分成4份,每次拿出一份做验证集,最后将验证分数求平均(期望),每次得到的验证分数可以用于优化模型。调整模型超参数时,比较不同超参数的验证分数,选择最优的超参数,这个模型作为最终的模型。比如,在训练分类器时,在不同的学习率、正则化系数等各种超参数进行K折实验,并记录每种超参数的平均验证分数,平均验证分数最高的超参数,作为最终的超参数。
2.7提前终止
提前终止(Early Stopping),模型对训练数据集迭代收敛之前停止迭代来防止过拟合,如果发现在验证集上测试误差上升,则停止训练。
3.过拟合欠拟合示例
3.1导入库
首先是导入必要的库torch
、numpy
、matplotlib
,Dataloader
和TensorDataset
主要用于构造数据加载器,train_test_split
用于划分训练集和测试集。
# 导入必要的库
import numpy as np
import torch
import torch.nn as nn
import matplotlib.pyplot as plt #用于数据可视化
from torch.utils.data import DataLoader, TensorDataset #用于构造数据加载器
from sklearn.model_selection import train_test_split#用于数据划分
3.2数据生成
设置随机数种子的目的是为了让每次生成的随机数是相同的,便于复现;np.random.uniform()
生成均匀分布的随机数组;np.random.normal()
生成均值为0,标准差为1的正态分布随机数;;torch.from_numpy(X).float()
将X转从Numpy数据类型转换为Pytorch的浮点型变量;plt.sacttter
绘制散点图,并用plt.show()
方法显示出来。
# 设置随机数种子
np.random.seed(2)
# 生成满足 y=x^2+1的数据
num_samples = 100 # 100个样本点
X = np.random.uniform(-5, 5, (num_samples, 1)) # 均匀分布
Y = X**2+1+5*np.random.normal(0,1,(num_samples,1))#正态分布噪声
# 将Numpy变量转化为浮点型Pytorch变量
X = torch.from_numpy(X).float()
Y = torch.from_numpy(Y).float()
# 绘制数据散点图
plt.scatter(X, Y)
plt.show()
3.3数据划分
为了便于后续效果评估,需要区分训练集和测试集。train_test_split(X, Y, test_size=0.3,random_state=0)
将数据拆分为训练集和测试集,其中测试数据占比0.3;TensorDataset
将训练集和测试集分别转换为data_set的形式,再使用DataLoader
封装成数据加载器,其中batch_size为32,shuffle表示是否打乱数据的顺序,一般训练数据集需要打乱,以提高模型的泛化能力。
# 将数据拆分为训练集和测试集
train_X, test_X, train_Y, test_Y = train_test_split(X, Y, test_size=0.3,random_state=0)
# 将数据封装成数据加载器
train_dataloader = DataLoader(TensorDataset(train_X, train_Y), batch_size=32, shuffle=True)
test_dataloader = DataLoader(TensorDataset(test_X, test_Y), batch_size=32, shuffle=False)
3.4模型定义
定义三种不同的模型,分别对应过拟合、正常和欠拟合。 欠拟合使用nn.Linear(1,1)
一个线性回归模型,输入维度为1,输出维度为1, 线性回归模型没有隐藏层,也没有激活函数,所以它肯定无法很好地拟合抛物线数据;正常情况下使用包含8个神经元的隐藏层,输入输出维度仍都是1;过拟合用一个比正常情况更加复杂的多层感知机,神经元每一层有256个,隐藏层的激活函数都是用的ReLU函数。
# 定义线性回归模型(欠拟合)
def plot_errors(models, num_epochs, train_dataloader, test_dataloader):
# 定义损失函数
loss_fn = nn.MSELoss()
# 定义训练和测试误差数组
train_losses = []
test_losses = []
# 遍历每类模型
for model in models:
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.005)
# 每类模型的训练和测试误差
train_losses_per_model = []
test_losses_per_model = []
# 迭代训练
for epoch in range(num_epochs):
# 在训练数据上迭代
model.train()
train_loss = 0
# 遍历训练集
for inputs, targets in train_dataloader:
# 预测、损失函数、反向传播
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_fn(outputs, targets)
loss.backward()
optimizer.step()
# 记录loss
train_loss += loss.item()
# 计算loss并记录
train_loss /= len(train_dataloader)
train_losses_per_model.append(train_loss)
# 在测试数据上评估,测试模型不计算梯度
model.eval()
test_loss = 0
with torch.no_grad():
# 遍历测试集
for inputs, targets in test_dataloader:
# 预测、损失函数
outputs = model(inputs)
loss = loss_fn(outputs, targets)
# 记录loss
test_loss += loss.item()
# 计算loss并记录
test_loss /= len(test_dataloader)
test_losses_per_model.append(test_loss)
# 记录当前模型每轮的训练测试误差
train_losses.append(train_losses_per_model)
test_losses.append(test_losses_per_model)
return train_losses, test_losses
# 定义更复杂的多层感知机(过拟合)
class MLPOverfitting(nn.Module):
def __init__(self):
super().__init__()
self.hidden1 = nn.Linear(1, 256)
self.hodden2 = nn.Linear(256, 256)
self.ouput = nn.Linear(256, 1)
def forward(self, x):
x = torch.relu(self.hidden1(x))
x = torch.relu(self.hidden2(x))
return self.output(x)
3.5辅助函数
定义一个函数,用于记录三个模型训练过程中的误差序列,参数包括模型、训练周期、训练数据和测试数据。误差函数使用均方误差MSE作为损失函数;torch.optim.SGD(model.parameters(), lr=0.005)
定义优化器SGD,学习率设置为0.005;定义两个数组用于记录每个模型的训练和测试误差;接下来进行迭代训练,每一轮遍历训练数据集,计算损失函数并反向传播、更新参数;然后再测试集上进行评估,遍历测试数据集进行预测和计算损失函数,并记录测试的损失。
def plot_errors(models, num_epochs, train_dataloader, test_dataloader):
# 定义损失函数
loss_fn = nn.MSELoss()
# 定义训练和测试误差数组
train_losses = []
test_losses = []
# 遍历每类模型
for model in models:
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.005)
# 每类模型的训练和测试误差
train_losses_per_model = []
test_losses_per_model = []
# 迭代训练
for epoch in range(num_epochs):
# 在训练数据上迭代
model.train()
train_loss = 0
# 遍历训练集
for inputs, targets in train_dataloader:
# 预测、损失函数、反向传播
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_fn(outputs, targets)
loss.backward()
optimizer.step()
# 记录loss
train_loss += loss.item()
# 计算loss并记录
train_loss /= len(train_dataloader)
train_losses_per_model.append(train_loss)
# 在测试数据上评估,测试模型不计算梯度
model.eval()
test_loss = 0
with torch.no_grad():
# 遍历测试集
for inputs, targets in test_dataloader:
# 预测、损失函数
outputs = model(inputs)
loss = loss_fn(outputs, targets)
# 记录loss
test_loss += loss.item()
# 计算loss并记录
test_loss /= len(test_dataloader)
test_losses_per_model.append(test_loss)
# 记录当前模型每轮的训练测试误差
train_losses.append(train_losses_per_model)
test_losses.append(test_losses_per_model)
return train_losses, test_losses
接下来指定训练周期num_epochs
,将models定义为前面定义的三种模型,然后之间将参数传入函数,就可以得到训练和测试误差的曲线数据。
# 获取训练和测试误差曲线数据
num_epochs = 200
models = [LinearRegression(), MLP(), MLPOverfitting()]
train_losses, test_losses = plot_errors(models, num_epochs, train_dataloader, test_dataloader)
3.6可视化
遍历三类模型,使用matplotlib
来绘制训练和测试误差曲线。欠拟合模型,再训练数据和测试数据上都不佳,无法很好地拟合数据;正常情况下,训练和测试曲线都随着训练地进行逐渐下降,并且文档在以恶搞较低地损失值。过拟合在测试集上表现差,训练出现不稳定,甚至随着训练次数的增加,误差反而增大。
# 绘制训练和测试误差曲线
for i, model in enumerate(models):
plt.figure(figsize=(8, 4))
plt.plot(range(num_epochs), train_losses[i], label=f"Train {model.__class__.__name__}")
plt.plot(range(num_epochs), test_losses[i], label=f"Test {model.__class__.__name__}")
plt.legend()
plt.ylim((0,200))
plt.show()
4.正则化
在机器学习中,有很多策略被显示用来减少测试误差, 这些策略统称为正则化(Regularization)。前面提到的解决过拟合的方法,例如图像增强,其实也可以任务是广义上的一种正则化的方法。
4.1深度学习中的正则化
标准定义:对学习算法的修改,目的是减少泛化误差,而不是训练误差。正则化有狭义和广义之分,广义上它是一系列具有正则化效果或者能够减少泛化误差的策略的统称。L2和L1正则这两种方法是狭义上的正则化,就是在原始问题的目标函数上,加一个正则项,使得求得的解更加稳定。
4.2没有免费午餐定理
没有免费午餐定理,可以说是正则化的理论指导。没有一种算法或者模型能够在所有的场景中都表现良好(需要根据问题的特征来选择合适的算法和模型,并且不断优化和调整),正则化是一种权衡过拟合和欠拟合的手段。
如下最左图所示,里面有两个模型A、B,分别能够对样本点进行拟合,哪个拟合地更好,仅从这几个数据点上来看,不好判断;以中间的图为例,新的数据点为圆圈点,这种情况A曲线拟合更好,但是换一组如右图所示数据,B曲线拟合得更好。
脱离具体的问题,空谈什么学习算法更好是没有意义的,因此在正则化的过程中,需要根据具体的情况来选择最合适的正则化方法。
4.3L2正则化
通过给模型的损失函数添加一个模型参数的平方和的惩罚项来实现正则化。
L
o
s
s
=
L
o
s
s
o
r
i
g
i
n
a
l
+
λ
∑
i
=
1
n
w
i
2
\begin{align*} Loss=Loss_{original} + \lambda\sum_{i=1}^nw_i^2 \end{align*}
Loss=Lossoriginal+λi=1∑nwi2
在机器学习中,这种正则化方法常常被称为岭回归,惩罚性
λ
∑
i
=
1
n
w
i
2
\lambda\sum_{i=1}^nw_i^2
λ∑i=1nwi2就是在搜素过程中给模型参数一个惩罚,如果参数数值过大,惩罚项也会很大,从而使得模型参数值趋于平稳,防止出现过拟合。
4.3.1L2正则化空间解释
L2正则化空间解释如下所示,选择均方误差MSE为损失函数,后面的正则项
λ
2
\frac{\lambda}{2}
2λ的
1
2
\frac{1}{2}
21主要是为了求导方便,与后面的平方约掉,其实加不加系数是无所谓的。
MSE是
w
w
w的平方和,在平面中有两个特征,其中
w
w
w是二维的,第三个坐标是损失函数MSE,在空间中的形状就是一个抛物面。后面的惩罚项或者正则项,是一个圆柱体,因为平方的展开
w
1
2
+
w
2
2
w_1^2+w_2^2
w12+w22小于某一个值。
w
∗
=
a
r
g
m
i
n
w
{
M
S
E
(
y
,
y
^
,
w
)
+
λ
2
∑
i
=
1
n
w
i
2
}
\begin{align*} w^*= \mathop{argmin}\limits_{w} \{MSE(y,\hat{y},\textbf{w})+\frac{\lambda}{2}\sum_{i=1}^{n}\textbf{w}_i^2\} \end{align*}
w∗=wargmin{MSE(y,y^,w)+2λi=1∑nwi2}
如果没有惩罚项,就需要找到抛物面的最低点,加上L2正则项时,就是找到它们的交点。正则化相当于给最小化加了一个空间约束,限制了模型的复杂度。
4.3L1正则化
L1正则化,又名LASS回归,具体来说,在损失函数中,加入模型参数权值矩阵中的各个元素的绝对值之和,
λ
\lambda
λ依然是超参数,用来控制正则化的强度,和L2正则化相比,L1正则化的效果是使权值矩阵
w
w
w的元素尽可能多地等于0,从而达到稀疏化。
L
o
s
s
=
L
o
s
s
o
r
i
g
i
n
a
l
+
λ
∑
i
=
1
n
(
∣
w
i
∣
)
\begin{align*} Loss=Loss_{original} + \lambda\sum_{i=1}^n(|w_i|) \end{align*}
Loss=Lossoriginal+λi=1∑n(∣wi∣)
L1正则化也被称为L1范数正则化,因为绝对值是范数为1时的正则化方法。
4.3L1正则化空间解释
以有两个特征为例,一个是
w
1
w_1
w1一个是
w
2
w_2
w2,损失函数还是使用均方误差MSE。 在三维坐标系中,均方误差是一个碗状抛物面,正常情况是将损失降到最低,找到其最低点。正则项
∣
w
i
∣
|w_i|
∣wi∣表示一个长方体。
w
∗
=
a
r
g
m
i
n
w
{
M
S
E
(
y
,
y
^
,
w
)
+
λ
∑
i
=
1
n
|w
i
∣
}
\begin{align*} w^*= \mathop{argmin}\limits_{w} \{MSE(y,\hat{y},\textbf{w})+\lambda\sum_{i=1}^{n}\textbf{|w}_i|\} \end{align*}
w∗=wargmin{MSE(y,y^,w)+λi=1∑n|wi∣}
下面右图俯视图可以看到,当二者相交时,
w
w
w有一个轴为0,那么参数矩阵更加稀疏。
4.4L1和L2正则化异同对比
L1正则化更倾向于产生稀疏解(参数权值变为0),适用于特征选择;L2正则化更倾向于小的非零权值,更适用于优化文图,使得权值更加平滑。
在选择正则化方法和调整正则化系数时,要根据数据的特征和问题的特征来进行选择。当然可以同时使用L1和L2正则化。
4.5范数惩罚
正则化不只局限于L1和L2正则化,完全可以扩展到一般情况。如下式:
L
o
s
s
=
L
o
s
s
o
r
i
g
i
n
a
l
+
λ
Ω
(
w
)
\begin{align*} Loss=Loss_{original} + \lambda\Omega(w) \end{align*}
Loss=Lossoriginal+λΩ(w)
其中,
λ
\lambda
λ是正则项系数,其大于等于0,
Ω
(
w
)
\Omega(w)
Ω(w)是惩罚项或者说是正则项,既可以是L1正则项,也可以是L2正则项。
λ
=
0
\lambda=0
λ=0,表示没有正则化,退化成一般情况,
λ
>
0
\lambda>0
λ>0时,
λ
\lambda
λ越大,起到作用越大。在神经网络中,一般参数包含
w
w
w和
b
b
b两种,
w
w
w是仿射变换权重,
b
b
b 偏置,通常情况下只考虑对
w
w
w做惩罚,因为拟合偏置
b
b
b时,所需要的数据量一般比较少,所以重点考虑
w
w
w。
4.6权重衰减
权重衰减(Weight Decay),也是一种正则化方法,但是不是在损失函数做文章,而是直接修改了最优化的过程中参数迭代的方程。
w
t
=
(
1
−
λ
)
w
t
−
1
−
α
Δ
L
\begin{align*} w_t=(1-\lambda)w_{t-1}-\alpha \Delta L \end{align*}
wt=(1−λ)wt−1−αΔL
其中,
w
t
w_t
wt是当前时刻的权重矩阵,
w
t
−
1
w_{t-1}
wt−1是前一时刻的权重矩阵,
α
\alpha
α是学习率,对损失函数求偏导数
∂
L
∂
w
\frac{\partial L}{\partial w}
∂w∂L得到
Δ
L
\Delta L
ΔL,正常情况下没有
1
−
λ
1-\lambda
1−λ进行了权重的衰减,可以证明当使用随机梯度下降法(Stochastic Gradient Descent,SGD),此时的方法等价于L2正则化,权重衰减和正则化本质上不是同一回事,权值衰减修改的是更新参数迭代的式子,而L2迭代是修改的损失函数。
5.Dropout方法
到目前为止,已经学习了五种正则化方法,数据增广、提前终止、L1正则化、L2正则化、权重衰减,那么Dropout方式是第六种。这些方法都是在解决过拟合问题、提升模型的泛化能力(数据和模型匹配度的问题),从数据、模型、训练过程去考虑,数据增广是从数据角度,提前终止和权重衰减是训练过程角度,L1正则化、L2正则化、范数惩罚是从模型角度。
Dropout方法,是研究模型结构如何调整。
5.1工作原理
在训练过程中随机“删除”(将其权重设为0)一些神经元,从而使这个模型不能够完全依赖于某些特征,就可以防止神经网络对训练集过于依赖,使得模型更加泛化。只在训练期间,不用在测试期间。
在测试期间,还需要用全部特征,也就是原来整个神经网络来进行测试。
5.2主要步骤
1.指定一个保留比例p,p一般会选择0.5或0.3;
2.每层每个神经元,以p的概率保留,以1-p的概率将权重设为零,在使用Dropout时就相当于训练了多个不同的子网络;
3.训练中使用保留的神经元进行前向、反向传播;
4.测试过程,将所有权重乘以p(因为每个神经元在训练过程中的期望输出是原来的p倍)。
5.3直观理解Dropout
Dropout方法可以类比成一个集成了大量神经网络的Bagging方法,对一堆数据每次有放回的选取,比如100个数,每次选10个取出,再放回去,每次都在这100个中选取。
相当于把一个网络进行了拆分,如下图所示,五个神经元组成的网络进行了拆分,拆成了16中组合方式,其中有些是没有办法链接的,用剩下的去训练。多个子网络构成一种集成学习的方式。
5.4Dropout在神经网络中的使用
Dropout在神经网络中如何使用呢?前面提到是让权重值等于0。
那么代码层该如何实现呢?在训练网络的每个单元时,添加一道计算概率的流程。下图右图是添加好后的图,
r
r
r取值要么是1要么是0,如果代码实现让某个神经元以概率p停止工作,就让他的激活函数乘以概率p变为0。
加入一层神经元的个数有1000个,Dropout的比例选择0.3,大大约300个值变为0。
5.5Dropout为什么能减少过拟合
本质上是Bagging集成学习,平均化作用。将包含多个子网络的集成神经网络拆完后,综合起来取平均的平均化策略,往往可以有效防止或减轻过拟合现象。因为不同的网络,产生不同的过拟合,取了平均,可以让一些相反的拟合相互抵消 ;
减少神经元之间复杂的关系,原来的神经网络可能很复杂,Dropout方法导致两个神经元不一定在同一子网络中,权值的更新不再依赖网络本身的连接关系,迫使神经网络去学习一些更加稳定的、鲁棒的数据特征;
类似性别在生物进化的角色。
5.5Dropout优缺点
优点:可以有效地减少过拟合(随机清零一定比例地输入特征,从而使模型不能够依赖于任何一个特定地输入特征,从而使模型更加稳健,在新数据上性能更好);
简单方便(将很多神经元系数或者说是权重置0);
实用有效(相比于其他计算开销小的正则化方法)。
缺点:降低训练效率(因为多了拆分、计算概率);
损失函数不够明确(模型拆分后,损失函数会有一些变化)。随机消除一些神经元地影响,导致损失函数有一些在计算上的误差。但是多数情况,只要能解决过拟合问题,就不会考虑效率、损失函数上的影响
6.Dropout的代码实现
6.1导入必要的库
设置随机数种子,便于复现;定义超参数样本数为20,隐藏层大小为200,训练次数为500。
# 导入必要的库
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# 随机数种子
torch.manual_seed(2333)
# 定义超参数
num_samples = 20 # 样本数
hidden_size = 200 # 隐藏层大小
num_epochs = 500 # 训练轮数
6.2生成数据
生成两组随机数据,分别作为训练集和测试集,生成过程使用相同代码。使用torch.linspace(-1, 1, num_samples)
生成[1,1]均匀分布的向量,利用torch.unsqueeze
转换为二维张量。真值y_train是利用x_train加上torch.randn
生成的随机值。
利用matplotlib.scatter
绘制散点图,
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
# 生成训练集
x_train = torch.unsqueeze(torch.linspace(-1, 1, num_samples), 1)
y_train = x_train + 0.3 * torch.randn(num_samples, 1)
# 测试集
x_test = torch.unsqueeze(torch.linspace(-1, 1, num_samples), 1)
y_test = x_test + 0.3 * torch.randn(num_samples,1)
# 绘制训练集和测试集
plt.scatter(x_train, y_train, c='r', alpha=0.5, label='train')
plt.scatter(x_test, y_test, c='b', alpha=0.5, label='test')
plt.legend(loc='upper left')
plt.ylim(-2,2)
plt.show()
6.3定义模型
通过torch.nn.Sequential
创建网络,输入输出都是1。Dropout层一般放在Linear全连接层之后,用于在训练时将全连接层中的参数以一定的概率进行丢弃。
# 定义一个可能会过拟合的网络
net_overfitting = torch.nn.Sequential(
torch.nn.Linear(1, hidden_size),
torch.nn.ReLU(),
torch.nn.Linear(hidden_size, hidden_size),
torch.nn.ReLU(),
torch.nn.Linear(hidden_size, 1),
)
# 定义一个包含 Dropout 的网络
net_dropout = torch.nn.Sequential(
torch.nn.Linear(1, hidden_size),
torch.nn.Dropout(0.5), # p=0.5
torch.nn.ReLU(),
torch.nn.Linear(hidden_size, hidden_size),
torch.nn.Dropout(0.5), # p=0.5
torch.nn.ReLU(),
torch.nn.Linear(hidden_size, 1),
)
6.4训练模型
调用torch.optim.Adam
定义优化器,并将学习率设置成0.1,损失函数使用MSE。
# 定义优化器和损失函数
optimizer_overfitting = torch.optim.Adam(net_overfitting.parameters(), lr=0.1)
optimizer_dropout = torch.optim.Adam(net_dropout.parameters(), lr=0.01)
# 损失函数
criterion =nn.MSELoss()
# 分别进行训练
for i in range(num_epochs):
# overfitting的网络:预测、损失函数、反向传播
pre_overfitting = net_overfitting(x_train)
loss_overfitting = criterion(pre_overfitting, y_train)
optimizer_overfitting.zero_grad()
loss_overfitting.backward()
optimizer_overfitting.step()
# 包含dropout的网络:预测、损失函数、反向传播
pred_dropout = net_dropout(x_train)
loss_dropout = criterion(pred_dropout, y_train)
optimizer_dropout.zero_grad()
loss_dropout.backward()
optimizer_dropout.step()
6.5可视化
首先利用eval
方法将两个模型设置为评估模型,目的是为了在测试过程中不使用Dropout,
# 在测试过程中使用Dropout
net_overfitting.eval()
net_dropout.eval()
# 预测
test_pred_overfitting = net_overfitting(x_test)
test_pred_dropout = net_dropout(x_test)
# 绘制拟合效果
plt.scatter(x_train, y_train, c='r', alpha=0.3, label='train')
plt.scatter(x_test, y_test, c='b', alpha=0.3, label='train')
plt.plot(x_test, test_pred_overfitting.data.numpy(), 'r-', lw=2, label='overfitting')
plt.plot(x_test, test_pred_dropout.data.numpy(), 'b--', lw=2, label='dropout')
plt.legend(loc='upper left')
plt.ylim((-2,2))
plt.show()
7.梯度消失和梯度爆炸
实践证明,在处理复杂任务上,深度网络比浅层的网络具备更好的效果。目前优化神经网络的方向都是基于反向传播的思想,根据损失函数计算误差, 通过梯度的反向传播来指导神经网络进行权值或参数的更新和优化。
7.1梯度的重要性
神将网络就是非线性多元函数,每一个神经元都是一个线性模型,外面套了一个激活函数,就实现了非线性变换,多层网络连接起来就是一个复合的非线性多元函数;
优化模型就是找到合适的权重,最小化损失函数。
假设非线性多元函数为
f
(
x
)
f(x)
f(x),
g
(
x
)
g(x)
g(x)是最终的函数,最终的目的就是找到合适的权值,使得损失函数最小。
L
o
s
s
=
∣
∣
g
(
x
)
−
f
(
x
)
∣
∣
2
2
\begin{align*} Loss = ||g(x)-f(x)||^2_2 \end{align*}
Loss=∣∣g(x)−f(x)∣∣22
假设损失函数特征空间如下图所示,最优化问题采用梯度下降的方法,再合适不过了。
7.2反向传播的内在问题
梯度消失或爆炸的原因可以从两个方面分析,一是在神经网络反向传播过程中,内在的问题;二是采用了不合适的损失函数,例如传统的Sigmoid函数。
下图是一个四层隐藏层的全连接层网络,如果要更新其中某一层的权值信息,根据链式求导法则,需要先更新梯度信息,以第二个隐藏层为例,链式求导法则如下所示:
∂
L
∂
w
2
=
∂
L
∂
f
4
∂
f
4
∂
f
3
∂
f
3
∂
f
2
∂
f
2
∂
w
2
\begin{align*} \frac{\partial L}{\partial w_2} =\frac{\partial L}{\partial f_4}\frac{\partial f_4}{\partial f_3}\frac{\partial f_3}{\partial f_2}\frac{\partial f_2}{\partial w_2} \end{align*}
∂w2∂L=∂f4∂L∂f3∂f4∂f2∂f3∂w2∂f2
如果层数很多,其中式中每一项都大于1,当层数增加时,最终求得的梯度更新或发生梯度爆炸。如果式中每一项小于1,尤其是非常小式,梯度更新的信息会以指数的形式进行衰减,此时发生了梯度消失。梯度消失、梯度爆炸根本原因在于链式求导法则。
7.3梯度消失
激活函数的导数小于1容易发生梯度消失。下图Sigmoid函数导数是黄色曲线,梯度没有超过0.25,
经过链式求导法则后,很多个0.25相乘很容易发生梯度消失。改进版就是Tanh函数,其导数紫色曲线可以达到1,但是从整体上看,其导数都是小于1的,也有梯度消失的问题。
7.4梯度爆炸
在深层网络或者循环神经网络中,误差的梯度可在更新中累积,变成非常大,导致网络不稳定,在极端情况下,权重值变得非常大,以至于溢出,导致NaN这种值。
原因一:深层网络;
原因二:初始化权重
w
w
w的值过大。
训练过程中出现梯度爆炸或伴随一些细微的信号,例如,模型无法从训练数据中获得更新、可视化曲线发现损失出现显著的变化、训练过程中模型损失变成了NaN。
7.5解决方法
预训练加微调;梯度剪切、正则;ReLU激活函数;Batchnorm(Batch normalization),批规范化;残差结构。预训练加微调方法比较老,现在基本不用了。
7.5.1梯度裁剪
设置一个梯度剪切阈值,超过则将其强制限制在这个范围之内。在梯度消失和爆炸二者之间,大多数梯度消失会出现得多一点。
7.5.2ReLU激活函数
导数在正数部分恒等于1,无梯度消失。**ReLU函数计算方便,能够加速训练过程。**缺点是由于负数部分恒为0,导数也为0,会导致一些神经元无法被激活。
解决无法被激活的方式可以通过设置小的学习率来部分解决,人们为了解决ReLU函数这个缺点,提出了各种改进版的ReLU函数,例如Leaky ReLU,在负数部分加了一个很小的斜率,避免了负数部分为0.
7.5.3Batch Normalization
Batch Normalization(简称BN,又称Batchnorm,批规范化)是深度学习发展的重要成果之一,已经被广泛应用于各个网络中,具有加速网络收敛速度,提升网络训练稳定性的效果,本质上是解决了反向传播过程中的梯度问题。
BN将输出信号规范化到均值为0、方差为1,以保证网络的稳定性。具体来说就是对n张图片(1个Batch)的某一个通道进行标准化,如下图所示,只对蓝色通道进行规范化操作。
x
i
x_i
xi是蓝色的部分,作为输入,求均值、方差,代入式子规范化得到
x
^
i
\hat{x}_i
x^i。
将数据规整到统一的区间,减少数据的发散程度,从而降低网络训练(学习)的难度,基本原理是为了防止梯度弥散(举例:
(
0.9
)
30
≈
0.04
(0.9)^{30}\approx 0.04
(0.9)30≈0.04)。BN通过将输出规范化,使得原本会减小的输出变大,进行缩放,是一种非常有效的局部规范化方法。
7.5.3残差结构
残差结构从网络结构(连接方式)上解决梯度消失和梯度爆炸问题,跨层连接结构解决梯度消失问题。
残差结构的出现直接导致了Image Net比赛的终结,自从被提出后,几乎所有的深度网络都离不开残差的身影,可以轻松构建几百甚至上千层的网络,而不需要担心梯度消失。原因是ShortCut(跳线,又称残差捷径)起到了非常重要的作用。
根据下面的式子可知,短路机制,可以无损的传播梯度,把梯度分为直接跳线传导(导数为1)和经过网络传导。
8.模型文件的读写
模型参数需要进行保存,以便部署到不同的环境中,当一个模型的训练需要花费很长时间的时候,最好在训练过程中定时保存中间的结果,不管是硬件还是软件出现问题,又或者外部因素,断电等都可避免损失。
8.1单个张量的保存和加载
在深度学习中,模型参数一般都是张量形式,对于单个的张量Pytorch提供了直接的函数来进行读写。比如定义一个张量a,并打印输出结果:
import torch
a = torch.rand(6)
print(a)
#tensor([0.8146, 0.9527, 0.6799, 0.2660, 0.3664, 0.5697])
调用torch.save
来保存张量a,文件命名为”tensor_a“保存到model文件夹中。
torch.save(a,"model/tensor_a")
调用torch.load
老加载张量,传入的参数是文件路径和文件名。
torch.load("model/tensor_a")
8.2张量列表的保存和加载
torch.save
和torch.load
同样也支持张量列表:
a = torch.rand(6)
b = torch.rand(6)
c = torch.rand(6)
[a,b,c]
#[tensor([0.1445, 0.5408, 0.9461, 0.3609, 0.7801, 0.8396]),
#tensor([0.3869, 0.8622, 0.4294, 0.8832, 0.7586, 0.3853]),
#tensor([0.0930, 0.9247, 0.4988, 0.3541, 0.4547, 0.6563])]
torch.save([a,b,c],"model/tensor_abc")
torch.load("model/tensor_abc")
8.3张量字典的保存和加载
Pytorch支持多个张量以字典的形式进行存储。
tensor_dict = {'a':a,'b':b,'c':c}
tensor_dict
torch.save(tensor_dict, "model/tensor_dict")
torch.load("model/tensor_dict")
8.4以多层感知机为例进行模型保存和加载
之前完成的多层感知机全部代码如下:
# 导入包
import torch
from torchvision import datasets
#导入transformer将数据转成tensor
from torchvision import transforms
#导入神经网络
import torch.nn as nn
#定义优化器
import torch.optim as optim
train_data = datasets.MNIST(root="data/mnist",train=True,transform=transforms.ToTensor(),download=True)
test_data = datasets.MNIST(root="data/mnist",train=False,transform=transforms.ToTensor(),download=True)
batch_size = 100
train_loader = torch.utils.data.DataLoader(dataset=train_data,batch_size=batch_size,shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_data,batch_size=batch_size,shuffle=False)
# 定义MLP网络 继承nn.Module
class MLP(nn.Module):
# 初始化方法
# input_size输入数据的维度
# hidden_size隐藏层的大小
# num_classes输出分类的数量
def __init__(self, input_size, hidden_size, num_classes):
# 调用父类的初始化方法
super(MLP, self).__init__()
# 定义第1个全连接层
self.fc1 = nn.Linear(input_size, hidden_size)
# 定义激活函数
self.relu = nn.ReLU()
# 定义第2个全连接层
self.fc2 = nn.Linear(hidden_size,hidden_size)
# 定义第3个全连接层
self.fc3 = nn.Linear(hidden_size, num_classes)
# 定义forward函数
# x 输入的数据
def forward(self, x):
# 第一层运算
out = self.fc1(x)
# 将上一步的结果发送给激活函数
out = self.relu(out)
# 将上一步结果送给fc2
out = self.fc2(out)
# 同样将结果送给激活函数
out = self.relu(out)
# 将上一步结果传递给fc3
out = self.fc3(out)
# 返回结果
return out
# 定义参数
input_size = 28*28 #输入大小
hidden_size =512 #隐藏层大小
num_classes = 10 #输出大小(类别数)
# 初始化MLP
model = MLP(input_size, hidden_size, num_classes)
8.1方式1:保存参数
调用torch.save
保存model.state_dict(),路径是”odel/mlp_state_dict.pth“,官方推荐以”.pt“或".pth"为保存模型的文件扩展名。使用torch.load
传入文件路径,实例化一个MLP模型,调用load_state_dict
方法传入读取的参数。
# 保存模型参数
torch.save(model.state_dict(), "model/mlp_state_dict.pth")
# 读取模型保存的参数
mlp_state_dict = torch.load("model/mlp_state_dict.pth")
# 新实例化一个MLP模型
model_load = MLP(input_size,hidden_size,num_classes)
# 调用load_state_dict方法传入读取的参数
model_load.load_state_dict(mlp_state_dict)
这里使用torch.save
保存的是模型参数,而不是整个模型,因此在模型加载参数时,需要单独指定模型框架,并且要保证模型架构和保存参数时的模型架构一致,否则会导致模型参数加载失败。
8.2方式2:保存整个模型
保存和加载整个模型。使用torch.save
保传入模型和保存路径,加载时调用torch.load
方法。
# 保存整个模型
torch.save(model, "model/mlp_model.pth")
# 保存整个模型
torch.save(model, "model/mlp_model.pth")
这种方式最直观也最简单,但是不推荐使用,因为它有缺点,进行模型保存时,会把模型结构定义、文件路径记录下来,加载时,会根据路径解析文件、装载参数,当模型定义、文件路径修改后,使用torch.load
加载模型就会报错。
8.3方式3:checkpoint
Pytorch提供了一种模型保存和加载的方式,叫做checkpoint,这种方式保存的是一个字典,如果程序在运行过程中由于某些原因异常终止,这种方式可以方便让我们接着上次继续训练,保存和加载的方式如下:
# 保存参数
torch.save({
'epoch':epoch,
'model_state_dict':model.state_dict(),
'optimizer_state_dict':optimizer.state_dict(),
'loss':loss,
...
},PATH)
# 加载参数
model = TheModeClass(*args, **kwargs)
optimizer = TheOptimizerClass(*args, **kwargs)
checkpoint = torch.load(PATH)
mode.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']
model.eval()