针对模型训练时出现的过拟合问题,限制模型容量是一种很好的解决方法,目前常用的方法有以下两种:
上图中,绿线部分为 L ( w , b ) L(w,b) L(w,b),在未引入正则惩罚项 λ 2 ∣ ∣ w ∣ ∣ 2 \frac {\lambda}{2}||w||^2 2λ∣∣w∣∣2 时, L L L的最优解在圆心(绿点)处,而引入正则惩罚项后,绿点处的值相对于黄线部分来说非常大,此时不再是最优解。
当 w ~ ∗ \tilde{w}^* w~∗向原点处移动时, L ( w , b ) L(w,b) L(w,b)的值会增大,但惩罚项的值会缩小(离原点越近,正则项越小)。因此在绿点处,惩罚项对最优点的拉动力大于 L ( w , b ) L(w,b) L(w,b) 的拉力,使得最优点逐渐移动到黄点位置,假设在此时, L ( w , b ) L(w,b) L(w,b)和惩罚项达到平衡,两侧的减少程度不足以满足另一项目的增加程度,此时达到最优。在这个过程中, w w w的绝对值不断降低,但不会像 L 1 L1 L1正则化那样降低到 0,达到稀疏矩阵的效果。在这个过程中,越小的参数就会带来越简单的模型。
1.“为什么越小的参数值会带来越简单的模型?”
答: 一方面,较大的参数值会放大相似输入之间的差异,使得网络对训练数据敏感,从而出现过拟合现象,而小权重缩小了这种差异,从而提供了更好的泛化性。另一方面,越复杂的模型,越倾向于拟合全部样本,甚至异常样本,此时这种拟合会使得函数曲线变化剧烈,因此复杂的模型往往是过拟合的结果,其参数值就会较大。
2.“为什么切点处是最小值?”
答: 采用拉格朗日乘子法求解约束极值问题时,例如 m i n L ( w , b ) s u b j e c t t o ∣ ∣ w ∣ ∣ 2 ≤ θ min \ L(w,b) \quad subject \ to \ ||w||^2 \leq \theta min L(w,b)subject to ∣∣w∣∣2≤θ,通常会引入拉格朗日乘子构建新的函数 L ( w , b , λ ) = L ( w , b ) + λ g ( w ) L(w,b, \lambda) = L(w,b) + \lambda g(w) L(w,b,λ)=L(w,b)+λg(w),然后通过求解下述方程组,可得最优解:
{ ▽ w L ( w , b , λ ) = L w ′ ( w , b ) = 0 ▽ b L ( w , b , λ ) = L b ′ ( w , b ) = 0 L λ ′ = g ( w ) = 0 \begin{cases} \triangledown_w L(w,b,\lambda) = L_w'(w,b)=0 \\ \triangledown_b L(w,b,\lambda) = L_b'(w,b)=0 \\ L_\lambda'= g(w) = 0 \end{cases} ⎩ ⎨ ⎧▽wL(w,b,λ)=Lw′(w,b)=0▽bL(w,b,λ)=Lb′(w,b)=0Lλ′=g(w)=0
当曲面 L ( w , b ) L(w,b) L(w,b)即图中绿色等高线部分与曲线 g ( w ) g(w) g(w)即图中黄色部分相切时,切点即为最优解 P P P,因为二者相切时,约束曲线 g ( w ) g(w) g(w) 在 P P P 处法向量 ▽ g \triangledown g ▽g与该处目标函数的梯度 ▽ L \triangledown L ▽L相等且反向(共线),也就是 L ( w , b ) L(w,b) L(w,b)和惩罚项达到平衡时。
未引入惩罚项时, w w w的梯度为 ∂ L ( w , b ) ∂ w \frac{\partial \ L(w,b)}{\partial \ w} ∂ w∂ L(w,b),时间 t t t 处更新参数公式为 w t + 1 = w t − η ∂ L ( w , b ) ∂ w w_{t+1}=w_t - \eta \frac{\partial \ L(w,b)}{\partial \ w} wt+1=wt−η∂ w∂ L(w,b)。
因此引入惩罚项后, w t w_t wt前增加了 ( 1 − η λ ) (1-\eta \lambda) (1−ηλ)项目,通常 η λ < 1 \eta \lambda < 1 ηλ<1,使得每一次更新时,当前的权重 w t w_t wt会进行一次缩小,这种情况就是 权重衰退。
总结:
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
初始化模型参数,并手动生成训练样本数为20,测试样本数为100,输入维度为200,batch_size为5的数据集,过小的训练样本集与过大的输入维度,可以使得模型过拟合效果更加明显,数据集的生成公式为:
y = 0.05 + ∑ i = 1 d 0.01 x i + ϵ w h e r e ϵ ∼ N ( 0 , 0.0 1 2 ) y = 0.05 + \sum_{i=1}^d 0.01x_i + \epsilon \qquad where \quad \epsilon \sim N(0, 0.01^2) y=0.05+∑i=1d0.01xi+ϵwhereϵ∼N(0,0.012)
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train) # 生成满足 y = Xw + b + noise的值
train_iter = d2l.load_array(train_data, batch_size) # 按batch_size将训练数据划分为iter
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)
对模型参数进行初始化,w满足正态分布,b为全零矩阵
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
定义 L2 惩罚项,为方便修改 λ \lambda λ 值,不将该超参数写入函数内。
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2 # ||w||^2 / 2
训练代码实现
def train(lambd):
w, b = init_params()
# 模型为一个简单的线性回归函数,loss函数为均方误差MSE
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003 # 定义epoch与学习率
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:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
# L = L(w,b) + lambda / 2 * ||w||^2
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward() # 反向传播
d2l.sgd([w, b], lr, batch_size) # 采用SGD优化函数作为优化器
if (epoch + 1) % 5 == 0:
# 每5轮在图像上绘制出一个最新的loss点
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数是:', torch.norm(w).item())
train(lambd=0)
此时惩罚项并未发挥作用,随epoch增加,train_loss与test_loss之间的差距越来越大,此时模型发生了过拟合。
train(lambd=3)
此时可以看到,随epoch的增加,train_loss与test_loss之间的差距开始缩小,证明增加惩罚项后,模型的过拟合现象得到缓解。
train(lambd=10)
相较于 λ = 3 \lambda = 3 λ=3,此时模型的train_loss与test_loss曲线间的差距进一步减少,并且都发生了很好的收敛,有效缓解了过拟合现象。
train(lambd=100)
再次增大 λ \lambda λ后,w的L2范数降低到非常小的值,此时发现train_loss与test_loss之前趋于稳定。
def train(epoch_iter):
lambd = 0
num_epochs, lr = 100, 0.003 # 定义epoch与学习率
animator = d2l.Animator(xlabel='lambd', ylabel='GAP', yscale='log',
xlim=[5, num_epochs], legend='GAP') # 绘制图像
for e_iter in range(epoch_iter):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
for epoch in range(num_epochs):
for X, y in train_iter:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w) # L = L(w,b) + lambda / 2 * ||w||^2
l.sum().backward() # 反向传播
d2l.sgd([w, b], lr, batch_size) # 采用SGD优化函数作为优化器
lambd = lambd + 10
animator.add(lambd, (d2l.evaluate_loss(net, test_iter, loss) - d2l.evaluate_loss(net, train_iter, loss)))
train(epoch_iter=10)
可以看到,从上图中可以看到,随 λ \lambda λ 的增加,train_loss与test_loss之间的差距不断缩小,直到 λ = 80 \lambda=80 λ=80左右时,达到稳定。
正如一个好的身体可以抵抗多种病毒,一个好的模型也需要做到对多种输入数据所带来的扰动鲁棒。以图片为例,不管图中加入多少噪音,在模糊一点的情况下,人依旧能识别出这张图片,忽略噪声带来的干扰。深度学习模型也是如此。
因此,一个好的模型能在未知数据上具备很好的泛化性,需要满足两点要求:
克里斯托弗·毕晓普等人证明,对于数据来说,使用有噪声的数据,实质上等价于一个Tikhonov 正则,属于在数据中增加噪音的方法。用数学证实了“要求函数光滑”和“要求函数对输入的随机噪声具有适应性”之间的联系。
丢弃法则与上述数据噪音不同,丢弃法不在输入时加噪音,而是在层之间增加无差别噪音。 因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性。
对于数据中加噪音的方法,输入值 x x x增加噪音得到结果 x ′ x' x′,通常希望加入噪音后,数据的期望不变,即 E [ x ′ ] = x E[x']=x E[x′]=x。
而丢弃法则是对于概率 p p p,为每个元素增加如下扰动:
x i ′ = { 0 w i t h p x i 1 − p o t h e r w i s e x_i' = \begin{cases} 0 \qquad with \ p \\ \frac{x_i}{1-p} \qquad otherwise \end{cases} xi′={0with p1−pxiotherwise
此时仍满足期望不变性。
E [ x i ′ ] = p ∗ 0 + ( 1 − p ) ∗ x i 1 − p = x i E[x'_i] = p * 0 + (1-p)* \frac{x_i}{1-p} = x_i E[xi′]=p∗0+(1−p)∗1−pxi=xi
通常在训练使用时,Dropout常被作用于全连接层的隐藏层上:
在使用Dropout后,隐藏层中的部分节点可能被随机抛弃,设置为0。而且该过程是一个随机的过程,每次Dropout时所抛弃的节点不一定相同。
但是在推理时,通常不会使用Dropout,因为其本质也可理解为一个正则项,而正则项因为影响模型参数的更新,通常只在训练时使用。
因此在预测时,参数无需变化,Dropout输出的时其输入本身,即 h = D r o p o u t ( h ) h=Dropout(h) h=Dropout(h) ,从而保证确定性的输出结果。
而在早期Hinton提出的Dropout中,具体思路与当今方法并不相同,Hinton将之理解为一个类似集成学习的ensemble问题,通过将神经网络中节点随机去掉后,形成多个不同的子神经网络,然后集成这些网络结果做一个平均,获得最终结果。
后续实验中发现,Dropout的结果很符合正则项的结果,因此人们逐渐将其转为正则问题看待。(玄学)
总结:
import torch
from torch import nn
from d2l import torch as d2l
# 定义Dropout层
def dropout_layer(X, dropout):
# 丢弃概率不在(0,1)之间时,触发断言退出函数
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
# 生成mask矩阵,根据该矩阵将对应位置x值置0
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)
# 定义模型参数
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
# 设置丢弃率
dropout1, dropout2 = 0.2, 0.5
# 模型定义
class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training = True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()
# 前向传播
def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用dropout
if self.training == True:
# 在第一个全连接层之后添加一个dropout层
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个dropout层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out
net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
# 模型训练与测试
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
交换两个丢弃率后,图像如下:
可以看到将丢弃率由 【0.2,0.5】更换为 【0.5,0.2】后,test_loss发生了一些波动,但最终结果无显著差异。
为放大这一问题,将丢弃率由【0.1,0.9】修改为【0.9,0.1】,图像如下:
可以看到模型出现了明显的过拟合现象,train_loss也下降缓慢。
初步猜测,由于多层感知机中采用全连接方法,层数越多提取的特征越精细,模型学习到过于精细的特征后,就会对微小特征过分敏感,产生过拟合现象。而【0.1,0.9】抛弃率Dropout的使用,使得模型少量抛弃了前期粗糙的部分冗余特征或噪声,大量抛弃精细特征,提高了模型鲁棒性。而【0.9,0.1】抛弃率的使用,会使得模型大量丢弃前期采集到的特征,降低了模型鲁棒性,致使test_acc 不增反减,train_loss也过高导致无法运行,改为【0.8,0.2】时仍会出现类似问题。
个人认为,过高的抛弃率对多层感知机的第一层影响最大,大量信息的丢失会使得模型产生很差的训练效果。但同样的高抛弃率对第二个隐藏层(也可能是最后一个隐藏层)影响较小,产生类似减少一个隐藏层的效果,反而降低了模型复杂度。