过拟合、欠拟合及其解决方案
模型选择、过拟合和欠拟合
权重衰减
基本概念
权重衰减等价于 范数正则化(regularization)。
以线性回归中的线性回归损失函数为例
其中是权重参数,是偏差参数,样本的输入为,标签为,样本数为。将权重参数用向量表示,带有范数惩罚项的新损失函数为
其中超参数。
有了范数惩罚项后,在小批量随机梯度下降中,线性回归中权重和的迭代方式更改为
可见,范数正则化令权重和先自乘小于1的数,再减去不含惩罚项的梯度。因此,范数正则化又叫权重衰减。权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。
简洁实现方法
-
在随机梯度下降的函数
torch.optim.SGD()
中,可将参数weight_decay
设置为L2范数中的。注意,一般只对权重参数进行衰减而不对偏差参数衰减:optimizer_w = torch.optim.SGD(params=[net.weight], lr=lr, weight_decay=wd) # 对权重参数衰减 optimizer_b = torch.optim.SGD(params=[net.bias], lr=lr) # 不对偏差参数衰减
实现代码:
函数参数就是L2范数中的。
def fit_and_plot_pytorch(wd):
# 对权重参数衰减。权重名称一般是以weight结尾
net = nn.Linear(num_inputs, 1)
nn.init.normal_(net.weight, mean=0, std=1)
nn.init.normal_(net.bias, mean=0, std=1)
optimizer_w = torch.optim.SGD(params=[net.weight], lr=lr, weight_decay=wd) # 对权重参数衰减
optimizer_b = torch.optim.SGD(params=[net.bias], lr=lr) # 不对偏差参数衰减
train_ls, test_ls = [], []
for _ in range(num_epochs):
for X, y in train_iter:
l = loss(net(X), y).mean()
optimizer_w.zero_grad()
optimizer_b.zero_grad()
l.backward()
# 对两个optimizer实例分别调用step函数,从而分别更新权重和偏差
optimizer_w.step()
optimizer_b.step()
train_ls.append(loss(net(train_features), train_labels).mean().item())
test_ls.append(loss(net(test_features), test_labels).mean().item())
d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
range(1, num_epochs + 1), test_ls, ['train', 'test'])
print('L2 norm of w:', net.weight.data.norm().item())
丢弃法(dropout)
基本概念
多层感知机中神经网络图描述了一个单隐藏层的多层感知机。其中输入个数为4,隐藏单元个数为5,且隐藏单元()的计算表达式为
这里是激活函数,是输入,隐藏单元的权重参数为,偏差参数为。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为,那么有的概率会被清零,有的概率会除以做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量为0和1的概率分别为和。使用丢弃法时我们计算新的隐藏单元
由于,因此
即丢弃法不改变其输入的期望值。由于在训练中隐藏层神经元的丢弃是随机的,即都有可能被清零,输出层的计算无法过度依赖中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法。
从零开始的实现方法
-
实现的精髓:生成一个size与输入X相同的矩阵,各个元素的值是从区间[0, 1)的均匀分布中抽取的一组随机数。对于矩阵的各个元素,如果值小于保留率(1-p),则返回1,意思是保留这个结点;否则返回0,意思是丢弃这个结点。这样返回的是一个mask矩阵,用mask * X / (1-p) )的值作为输入喂入网络,就能实现”丢弃法“的效果。
mask = (torch.rand(X.shape) < keep_prob).float()
其中,keep_prob表示保留率(1-p)
torch.rand()
返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数。 -
在实现计算模型准确率的函数时要注意:如果采用pytorch的网络层类
torch.nn.Module
,那么要在计算准确率之前把网络改为评估模式,在计算之后再改回训练模式。- 神经网络模块存在两种模式,训练模式
net.train()
和评估模式net.eval()
。一般的神经网络中,这两种模式是一样的,只有当模型中存在dropout和batchnorm的时候才有区别。在评估模式中,会关闭dropout。
- 神经网络模块存在两种模式,训练模式
简洁实现
- 在
nn.Sequential()
中可直接添加nn.Dropout(丢弃率)
。
代码示例:
net = nn.Sequential(
d2l.FlattenLayer(),
nn.Linear(num_inputs, num_hiddens1),
nn.ReLU(),
nn.Dropout(drop_prob1), #drop_prob1和drop_prob2是预先设置好的丢弃率p
nn.Linear(num_hiddens1, num_hiddens2),
nn.ReLU(),
nn.Dropout(drop_prob2),
nn.Linear(num_hiddens2, 10)
)
for param in net.parameters():
nn.init.normal_(param, mean=0, std=0.01)
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer) #这是个写好的训练函数
梯度消失、梯度爆炸
随机初始化模型参数
“随机初始化”的意义:
如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。在线性回归的简洁实现中,我们使用
torch.nn.init.normal_()
使模型net
的权重参数采用正态分布的随机初始化方式。不过,PyTorch中nn.Module
的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑。Xavier随机初始化:另外的一种比较常用的随机初始化方法。
假设某全连接层的输入个数为,输出个数为,Xavier随机初始化将使该层中权重参数的每个元素都随机采样于均匀分布
它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数影响,且每层梯度的方差也不该受该层输出个数影响。
考虑到环境因素的其它问题
协变量偏移
输入的分布P(x)改变了,但标记函数,即条件分布P(y∣x)保持不变的情况,导致模型的测试结果不够理想。
举例:区分猫和狗的任务:训练数据使用的是猫和狗的真实的照片,但是在测试时被要求对猫和狗的卡通图片进行分类。
标签偏移
导致偏移的是标签P(y)上的边缘分布的变化,但类条件分布是不变的P(x∣y)时的情况。当我们认为y导致x时,标签偏移是一个合理的假设。
标签偏移可以简单理解为:测试时出现了训练时没有的标签。
举例:病因(要预测的诊断结果)导致 症状(观察到的结果)。训练数据集:数据很少只包含流感p(y)的样本。而测试数据集有流感p(y)和流感q(y),其中不变的是流感症状p(x|y)。
概念偏移
标签本身的定义发生变化的情况。
举例:在美国的不同地理位置,对“软饮料”这一概念的定义有差异。如果我们要建立一个机器翻译系统,分布P(y∣x)可能因我们的位置而异。
循环神经网络进阶
GRU(门控循环单元)
重置⻔有助于捕捉时间序列⾥短期的依赖关系;
更新⻔有助于捕捉时间序列⾥⻓期的依赖关系。-
需要初始化的参数:共12个
- 隐藏层的学习参数
- ,,:size(x,h),其中h是隐藏神经元个数
- ,,:size(h,h)
- , , :size(h)
- 输出层的学习参数
- :size(h,q) 其中q为输出类别数
- :size(q)
- 输入为第一个输入时
- 初始隐藏状态:一般初始化成0
- 隐藏层的学习参数
实现代码
部分实现代码:
#---载入数据集---
#……
#---初始化参数---
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
def get_params():
def _one(shape):
ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32) #正态分布
return torch.nn.Parameter(ts, requires_grad=True)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
W_xz, W_hz, b_z = _three() # 更新门参数
W_xr, W_hr, b_r = _three() # 重置门参数
W_xh, W_hh, b_h = _three() # 候选隐藏状态参数
# 输出层参数
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q])
def init_gru_state(batch_size, num_hiddens, device): #隐藏状态初始化
return (torch.zeros((batch_size, num_hiddens), device=device), )
#---定义GRU模型---
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
H_tilda = torch.tanh(torch.matmul(X, W_xh) + R * torch.matmul(H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
#---训练模型---
#……
简洁实现方式:
利用nn.GRU()
:
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
LSTM(长短期记忆网络)
-
长短期记忆long short-term memory:
- 遗忘门:控制上一时间步的记忆细胞
- 输入门:控制当前时间步的输入
- 输出门:控制从记忆细胞到隐藏状态
- 记忆细胞:⼀种特殊的隐藏状态的信息的流动
-
需要初始化的参数:共16个
- 隐藏层的学习参数
- ,,,:size(x,h),其中h是隐藏神经元个数
- ,,,:size(h,h)
- ,, ,:size(h)
- 输出层的学习参数
- :size(h,q),其中q为输出类别数
- :size(q)
- 输入为第一个输入时
- 初始记忆细胞:一般初始化成0
- 初始隐藏状态:一般初始化成0
- 隐藏层的学习参数
实现代码
部分实现代码:
#---载入数据集---
#……
#---初始化参数---
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
def get_params():
def _one(shape):
ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
return torch.nn.Parameter(ts, requires_grad=True)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
W_xi, W_hi, b_i = _three() # 输入门参数
W_xf, W_hf, b_f = _three() # 遗忘门参数
W_xo, W_ho, b_o = _three() # 输出门参数
W_xc, W_hc, b_c = _three() # 候选记忆细胞参数
# 输出层参数
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q])
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
#---定义LSTM模型---
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
#---训练模型---
#……
简洁实现方式:
利用nn.LSTM()
:
lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens)
深度循环神经网络
- 相当于从左到右把之前讨论的循环神经网络叠起来,前一层的隐藏状态作为下一层循环神经网络按时间的输入。
- 这样做的好处:可以利用深度的网络结构抽取到更抽象的信息。
实现方法
如隐藏层采用LSTM,则可如此实现:
gru_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens,num_layers=2)
想比之前单隐藏层的LSTM只是多设置了参数num_layers
。它表示的是隐藏层的层数,默认为1。此时设置为2,也就是说上面的图中从左到右白色的层一共有两层。
双向循环神经网络
- 两个隐藏层分别按两个相反的时间方向接收输入。两个隐藏层又会被拼接起来成为。
- 带来的好处:如果输入是一句话,可以同时考虑某个字前面以及后面的字对它造成的影响。
实现方法
如隐藏层采用GRU,则可如此实现:
gru_layer = nn.GRU(input_size=vocab_size,hidden_size=num_hiddens,bidirectional=True)
相比之前普通的GRU只是多设置了参数bidirectional
。它表示的是网络是否为双向的,默认为False。