从感知机到多层感知机:
感知机:只能产生线性分割面,不能拟合XOR
为突破线性模型的限制,可以通过在网络中加入一个/多个隐藏层,即 多层感知机MLP。但是如果只是单纯添加隐藏层,还是等价于一个线性模型(仿射变换的仿射变换还是仿射变换),没有带来益处!此时,需要加入额外因素以激发多层架构的潜力——对每个隐藏层使用非线性的激活函数,这样多层感知机就不会退化成线性模型
理论上,单隐藏层的网络可以学习任何函数,然而实际中我们并不这么用。因为一个浅但宽的网络很难训练,且易overfit;而一个更深的网络(每层学一点)相对容易训练
———————————————————————————————————
激活函数:
关于激活函数的选择,李沐老师说,激活函数的选择不是很重要,没主意的时候就用ReLU吧,毕竟简单!其他的超参数对于模型更重要些,比如隐藏层数、每层隐藏层的大小
1、ReLU
\(ReLU(x) = max(x,0)\)
(1)ReLU提供了一种非常简单的非线性变换(无指数运算,算起来快)
(2)ReLU的求导表现特别好,要么让参数消失——在输入<0时导数为0,要么让参数通过——输入>0时导数为1,这使得其优化表现很好;输入=0处不可导,但是因为输入不可能为0,可忽略这种情况
(3)ReLU还解决了sigmoid函数在两端梯度消失的问题
2、sigmoid函数
\(sigmoid(x) = \frac{1}{1+exp(-x)}\)
其形状很像一个soft版本的阶跃函数,符合人体神经元的特性。但是由于其两端太平(梯度~0),易造成梯度消失,在激活函数中已较少使用(RNN中还用,用于控制时序信息流)
*softmax和sigmoid:
softmax是线性变换,一般是多类别分类问题的输出层接softmax,使得每个输出在0-1内,和为1
softmax适用于多类别分类,一个正确答案,互斥输出
sigmoid适用于多标签分类,多个正确答案,非互斥输出
3、tanh函数
\(tanh(x) = \frac{1-exp(-2x)}{1+exp(-2x)}\)
把sigmoid原来(0,1)的范围拉到(-1,1)的区间内,且关于原点中心对称
代码实现:
在网络中加入隐藏层和激活函数也比较简单,nn.Sequential
里加layer就行:
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
————————————————————————————————————
训练误差:模型在训练集上计算得到的误差
泛化误差:模型应用于从原始样本的分布中抽取无限多数据样本时,模型误差的期望。难以准确计算,但可以用模型在一个独立测试集or验证集上的误差,即测试误差or验证误差,来估计泛化误差
监督学习场景中,我们假设训练集和测试集是独立同分布的,但事实上可能会轻微违背独立同分布,导致训练出的模型在没见过的数据(测试集or验证集)上表现不好,泛化性不佳
过拟合、欠拟合:模型容量(模型复杂度)& 数据复杂度 不匹配
模型复杂度的影响因素:1-参数数量多——模型复杂;2-参数的取值范围大——模型复杂
数据复杂度的影响因素:1-样本个数;2-每个样本的元素个数;3-时间、空间结构;4-多样性
在实际训模型时,我们通过观察训练误差和验证误差来判断有无欠拟合or过拟合
欠拟合:训练误差&验证误差都很大,模型无法减小训练误差。意味着,模型过于简单,无法捕捉数据pattern。这时候可以换一个更复杂的模型去降低训练误差
过拟合:训练误差明显小于验证误差。意味着,模型过于复杂但数据简单,模型过度关注数据细节甚至噪声,试图记住所有训练数据,偏离了数据本身的主要pattern,导致其在没见过的数据上表现不佳,泛化性不佳
可以看出,欠拟合&过拟合 取决于 模型容量=模型复杂度、数据复杂度
(1)模型复杂&数据量少,往往更易过拟合
(2)模型简单,易欠拟合(此时关键要提升模型复杂度,增加数据量没啥用)
但是,在深度学习领域,过拟合并不总是一件坏事!我们更关心验证误差是不是小,而不是训练误差和验证误差的差距是不是小
关于欠拟合和过拟合的其他解释,见我的另一博客
————————————————————————————————————
模型选择:
训练集:训练模型的参数
验证集:选择模型的超参数
如果数据集小,可以用k折交叉验证(多训几次,但是损失的数据少)
关于k折交叉验证的具体内容,见我的另一博客
————————————————————————————————————
正则化方法——防过拟合:权重衰减、drop out
(零)“正则regularization”的含义:对模型(参数)施加规则(限制)
(一)权重衰减
1、思路
防止过拟合需要限制模型容量,两个途径1-让参数量减少;2-让参数取值范围缩小
*参数范围大,会拟合成很复杂的函数,导致对训练数据过拟合。参数范围小一点的话,拟合曲线会更平滑
缩小参数取值范围的方式:
(1)硬性限制
\(min \quad l(w,b) \quad subject \quad to \quad \|w\|^2 \leqslant \theta\)
\(\theta\)越小,正则项越强
(2)柔性限制
可用拉格朗日乘子证明:使用均方范数作为硬性限制 等价于 如下柔性限制:
\(min \quad l(w,b)+\frac{\lambda}{2}\|w\|^2\)
相当于,原loss中加了L2正则项,用\(\lambda\)控制正则项重要程度,\(\frac{1}{2}\)为了方便之后求导算梯度时与平方抵消
2、参数更新法则:权重衰减
梯度:\(\frac{\partial{l(w,b)}}{\partial(w)}+\lambda{w}\)
更新参数:\(w_{t+1}=(1-\eta\lambda)w_{t}-\eta\frac{\partial{l(w_{t},b_{t})}}{\partial(w_{t})}\)
由于\(\eta\lambda < 1\),深度学习中常称之为权重衰减
3、更多思考
思考1:
在loss中加正则项以防止过拟合,在前文中,我们是根据缩小参数范围可以降低模型容量从而防止过拟合 + 拉格朗日乘子,去证明的
但是,反过来,我们需要思考,最小化原loss和正则项的和,到底有什么实际意义?
(1)可使参数形成稀疏解,从而降低模型复杂度,防过拟合。具体解释见我的另一个blog
(2)limu的解释:
绿线中心:原loss最小化的最优解。但因为数据往往有噪声,模型把自己搞得很复杂以过度记住/关注噪声——过拟合的原因,此时,模型以为的最优(拟合了大量噪声)其实不是最优
黄线中心:正则项最小化的最优解
在原loss中加入正则项后,绿中心不再是最优解,黄线的存在(正则项的存在)把最优解往左下拉(\(\lambda\)决定拉的程度),至两者相交处。这样防止了过拟合
思考2:
为什么深度学习中首先使用L2范数?
首先使用L2范数的一个原因是,它对权重向量的大分量施加巨大惩罚,使得学习算法偏向于 在大量特征上均匀分布权重 的模型(都接近0但不为0),这样对单个变量中的观测误差更为稳定
相比之下,L1范数会导致模型把权重集中于一小部分特征,而把其他的权重清零,这样的做法其实是“特征选择”,可能是其他场景下需要的。
比如在我的另一个blog里对正则化的解释,其实是以L1范数作为正则项为例的,因为那个blog的笔记来自于传统机器学习的课程
4、代码实现:
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd}, # 实例化优化器时直接通过weight_decay指定weight decay超参数
{"params":net[0].bias}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())
wd取值一般设置为1e-2,1e-3,1e-4。如果试了一遍都效果不好(特别是模型很复杂时),可能需要换个正则化方法,比如drop out
(二)drop out
1、思路
防过拟合
——>模型简单
——>模型简单的另一个角度要求“平滑性”
(这一步可以数学证明:要求函数平滑 等价于 要求函数对输入的随机噪声具有适应性)
——>对输入的微小变化不敏感,即,对输入添加随机噪声 不影响模型
如何向输入添加噪声?无偏差地加入噪声
对原输入\(x\)加入噪声,变成\(x^{'}\)
(1)每一层期望值不变\(E(x^{'})=x\)
(2)对每个元素产生扰动
上述公式在做的事情是,对一部分输入置零(概率为p),对剩下的输入除以1-p,以保证期望不变
——>从表面上看,是在训练过程中丢弃一些神经元(将当前层中的一些神经元的输出“置零”,等同于,将下一层中一些神经元的输入“置零”),so得名drop out丢弃法
*这里另一个思考点在于,数据本身往往有有偏好的噪声,加入随机噪声,有利于抵消上述偏好,使模型更鲁棒
2、如何实施drop out
一般将丢弃法作用于隐藏全连接层的输出上,只在训练中使用丢弃法\(h^{'}=dropout(h)\),推理中不使用——直接返回\(h=dropout(h)\),也保证了模型的确定性输出
*所有正则化方法都只在训练中使用,因为正则化是为了限制模型从而影响参数更新,而只有训练中涉及参数更新
3、更多思考
每次丢弃的神经元不同,可以理解为每次随机采样一个子神经网络进行训练,但在推理时使用所有的神经元(参数),这本质上是一种ensemble,大量dropout形成的很多个网络模型ensemble,会极大提升预测能力 (Hinton解释)
另一方面,每次随机丢弃一部分神经元(输入置零),避免模型过度依赖任何一个神经元,即,避免个别神经元的影响过大,即,个别参数过大的可能性减小
后来大家做实验,发现这个和正则的效果一样,so被认为是正则化的方法
4、代码实现
对于深度学习框架的高级API,我们只需在每个全连接层之后添加一个Dropout层, 将暂退概率作为唯一的参数传递给它的构造函数。 在训练时,Dropout层将根据指定的暂退概率随机丢弃上一层的输出(相当于下一层的输入)。 在测试时,Dropout层仅传递数据。
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
几个重要的点:
(1)nn.Dropout
(2)dropout用在隐藏全连接层之后
(3)丢弃仅发生于训练时
(4)超参数:唯一需要传入的参数是丢弃概率p,一般取0.1、0.5、0.9三种值
————————————————————————————————————
为什么会有数值稳定性问题?
d层的神经网络,计算梯度时,会涉及很多次矩阵的乘法。一旦出现很小或很大的数值,多次乘法后,会出现梯度消失or爆炸
数值稳定性的2个常见问题:梯度消失、梯度爆炸
(一)梯度消失
1、后果
(1)无论怎么调lr,参数不更新,学习没有进展
(2)对神经网络的底层尤为严重:因为梯度是反向传播计算的,从网络顶层到底层,顶层的时候可能梯度没乘几次,不会太小,训练效果可能还行,但底层会梯度为0,根本学习不下去,这时,和一个很浅的网络没区别
2、原因之一
sigmoid函数的输入很大或很小时,其梯度就会消失
解决:一般默认用更稳定的ReLU作为激活函数
(二)梯度爆炸
1、后果
(1)值超出值域,对16位浮点数尤为严重
(2)对lr过于敏感,可能需要训练中不断调整lr
2、原因之一
参数初始化过大
解决:对参数初始化进行限制——在合理区间内随机初始参数——使得每层的输出【正向】和梯度【反向】的分布被限制——均值为0,方差为常数
由数学推理可得Xaiver初始化方法(nn.Linear的默认初始化方法)
总之,让训练更加稳定,目标为使得梯度值在合理范围内,方法:1-乘法变加法,如ResNet、LSTM;2-梯度归一化、梯度裁剪;3-合理权重初始化;4-激活函数选择,如ReLU
——————————————————————————————————
QA:
1、SVM两个缺点:1-数据特别大的时候,算起来慢;2-可调的参数不多(确定性强,模型容量有限)
2、数据预处理,比如标准化要算均值和标准差,最好拿所有的数据一起算,这样比较鲁棒
3、深度学习数据量特别大时,不做交叉验证,要不然成本太高;传统机器学习,一般是做交叉验证的
4、超参数选择,沐神推荐随机100次选最好的
5、按理来说,单隐藏层的MLP可以拟合任何模型,但是其实训练不出来。我们设计深度学习里的各种模型,是在用先验知识去设计一个合适的模型结构——能够更好描述数据pattern——更易训练出来
6、不平衡的数据集,如果真实世界中数据本身就是不平衡的,那没关系,把主流做好就行。但是如果是采样不平衡,则要先平衡数据集(加权重,可以在数据上加权重,也可以在loss加权)
7、关于防止过拟合的调参,即使在验证集上试出了一个比较好的超参数,未必泛化性好。这时候可以往这个超参数周围调一调,如果很敏感,变化大,说明这一带区域不平坦,只是运气好,,这时候还要再调调。工程里,调参不太重要,大概调调就行,因为数据一直会变,在一份数据上调好的超参数,如果来了新的一批数据,未必是好的超参数
8、考虑一个实际问题的解决,思路:(1)ML能不能做,能不能解决这个问题;(2)我需要去收集什么样的数据(自己的数据质量高去得到好的结果,比用烂烂的数据训模型然后费尽心机调参,简单得多)
9、loss曲线可以抖,但不能只抖动不下降,如果光抖不降,考虑把Lr、batch_size调大一点
10、如果出现NaN,是因为除了0;如果出现Inf,是值太大,可能是lr太大,或者权重初始化太大,这时候先往下调,直到不再出现Inf