6.4 正则化
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。抑制过拟合的技巧也很重要。
6.4.1 过拟合
发生过拟合的原因,主要有以下两个。
• 模型拥有大量参数、表现力强。
• 训练数据少。
这里,我们故意满足这两个条件,制造过拟合现象。为此,要从MNIST数据集原本的60000个训练数据中只选定300个,并且,为了增加网络的复杂度,使用7层网络(每层有100个神经元,激活函数为ReLU)。
下面是用于实验的部分代码
接着是进行训练的代码。和之前的代码一样,按epoch分别算出所有训练数据和所有测试数据的识别精度。
# 1. 加载MNIST手写数字数据集,并做归一化处理
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 2. 【核心操作】为了人为再现过拟合现象,故意将训练数据从6万张减少到仅300张
# 这使得模型很容易“记住”这300个样本,但无法学会泛化到新数据的规则。
x_train = x_train[:300]
t_train = t_train[:300]
# 3. 初始化一个“大”模型
# 输入层784个神经元(28x28像素),5个隐藏层,每层100个神经元,输出层10个神经元(10个类别)
# 这个模型的参数很多,容量很大,在数据量很少时极易过拟合。
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10)
# 4. 选择优化器:使用随机梯度下降(SGD),学习率设为0.01
optimizer = SGD(lr=0.01)
# 5. 设置训练参数
max_epochs = 201 # 最大训练轮数(Epoch)
train_size = x_train.shape[0] # 当前训练集大小(300)
batch_size = 100 # 每个批次的样本数
train_loss_list = [] # 用于记录每个Epoch后的训练损失
train_acc_list = [] # 用于记录每个Epoch后的训练集准确率
test_acc_list = [] # 用于记录每个Epoch后的测试集准确率
# 6. 计算每个Epoch包含多少个迭代(Iteration)
# 因为训练数据只有300个,批次大小为100,所以每3个迭代就遍历了一遍全部数据,算作一个Epoch。
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
# 这是一个无限的训练循环 (for i in range(1000000000)),由后面的 break条件来控制退出。
for i in range(1000000000):
# 7. 随机抽取一个批次(Batch)的数据
# batch_mask 是一个包含100个随机索引的数组,用于从300个训练样本中随机选取100个。
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask] # 获取批次的输入数据
t_batch = t_train[batch_mask] # 获取批次的目标标签
# 8. 【核心计算】计算梯度
# 这是最关键的一步:通过网络的反向传播功能,计算当前批次数据下的损失函数关于所有参数(W, b)的梯度。
grads = network.gradient(x_batch, t_batch)
# 9. 【更新参数】使用优化器根据计算出的梯度更新网络参数
# optimizer.update() 方法会执行:params = params - lr * grads
optimizer.update(network.params, grads)
# 10. 检查是否完成了一个Epoch(即是否达到了每iter_per_epoch次迭代)
if i % iter_per_epoch == 0:
# 11. 评估模型性能
# 计算模型在当前所有300个训练数据上的准确率
train_acc = network.accuracy(x_train, t_train)
# 计算模型在所有10000个测试数据上的准确率
test_acc = network.accuracy(x_test, t_test)
# 12. 记录准确率,用于后续绘制学习曲线,观察过拟合
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
# 13. 增加Epoch计数器
epoch_cnt += 1
# 14. 退出条件:如果训练的Epoch数达到了预设的最大值,就跳出循环,结束训练。
if epoch_cnt >= max_epochs:
break
现在,我们将这些列表(train_acc_list、test_acc_list)绘成图,结果如图6-20所示。
过了 100 个 epoch 左右后,用训练数据测量到的识别精度几乎都为100%。但是,对于测试数据,离100%的识别精度还有较大的差距。如此大的识别精度差距,是只拟合了训练数据的结果。从图中可知,模型对训练时没有使用的一般数据(测试数据)拟合得不是很好。
6.4.2 权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是因为权重参数取值过大才发生的。
复习一下,神经网络的学习目的是减小损失函数的值。这时,例如为损失函数加上权重的平方范数(L2范数)。这样一来,就可以抑制权重变大。用符号表示的话,如果将权重记为W,L2范数的权值衰减就是
,然后将这个
加到损失函数上。这里,λ是控制正则化强度的超参数。λ设置得越大,对大的权重施加的惩罚就越重。
对于所有权重,权值衰减方法都会为损失函数加上
。因此,在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数λW。
L2范数相当于各个元素的平方和。用数学式表示的话,假设有权重W = (w1, w2, ... , wn),则 L2范数可用
计算出来。除了 L2范数,还有 L1范数、L∞范数等。L1范数是各个元素的绝对值之和,相当于|w1| + |w2| + ... + |wn|。L∞范数也称为Max范数,相当于各个元素的绝对值中最大的那一个。L2范数、L1范数、L∞范数都可以用作正则化项,它们各有各的特点,不过这里我们要实现的是比较常用的 L2范数。
现在我们来进行实验。对于刚刚进行的实验,应用λ = 0.1的权值衰减,结果如图6-21所示
如图6-21所示,虽然训练数据的识别精度和测试数据的识别精度之间有差距,但是与没有使用权值衰减的图6-20的结果相比,差距变小了。这说明过拟合受到了抑制。此外,还要注意,训练数据的识别精度没有达到100%(1.0)。
6.4.3 Dropout
作为抑制过拟合的方法,前面我们介绍了为损失函数加上权重的L2范数的权值衰减方法。该方法可以简单地实现,在某种程度上能够抑制过拟合。 但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情况下,我们经常会使用Dropout
Dropout是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递,如图6-22所示。训练时,每传递一次数据,就会随机选择要删除的神经元。然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。
下面我们来实现Dropout。这里的实现重视易理解性。不过,因为训练时如果进行恰当的计算的话,正向传播时单纯地传递数据就可以了(不用乘以删除比例),所以深度学习的框架中进行了这样的实现。关于高效的实现,可以参考Chainer中实现的Dropout。
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio # 丢弃概率,默认为50%
self.mask = None # 用于保存前向传播时生成的随机掩码
def forward(self, x, train_flg=True):
if train_flg: # 训练模式
# 核心步骤1:生成随机掩码。随机数大于dropout_ratio的位置为True(保留),否则为False(丢弃)
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
# 核心步骤2:将输入x与掩码相乘。被丢弃的神经元输出为0,保留的神经元输出保持不变
return x * self.mask
else: # 测试/推理模式
# 不丢弃任何神经元,但对所有输出进行缩放,乘以保留概率 (1 - dropout_ratio)
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
# 将上游传来的梯度 dout 与训练时保存的掩码相乘
# 这意味着,在前向传播中被丢弃的神经元(mask对应位置为False),其梯度也将被置为0
# 只有前向传播时被保留的神经元,才会参与误差的反向传播和参数更新
return dout * self.mask
这里的要点是,每次正向传播时,self.mask中都会以False的形式保存要删除的神经元。self.mask会随机生成和x形状相同的数组,并将值比dropout_ratio大的元素设为True。反向传播时的行为和ReLU相同。也就是说,正向传播时传递了信号的神经元,反向传播时按原样传递信号;正向传播时没有传递信号的神经元,反向传播时信号将停在那里。
现在,我们使用MNIST数据集进行验证,以确认Dropout的效果。
图6-23中,通过使用Dropout,训练数据和测试数据的识别精度的差距变小了。并且,训练数据也没有到达100%的识别精度。像这样,通过使用Dropout,即便是表现力强的网络,也可以抑制过拟合
6.5 超参数的验证
神经网络中,除了权重和偏置等参数,超参数(hyper-parameter)也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch大小、参数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型的性能就会很差。虽然超参数的取值非常重要,但是在决定超参数的过程中一般会伴随很多的试错。
6.5.1 验证数据
之前我们使用的数据集分成了训练数据和测试数据,训练数据用于学习,测试数据用于评估泛化能力。由此,就可以评估是否只过度拟合了训练数据(是否发生了过拟合),以及泛化能力如何等
用测试数据确认超参数的值的“好坏”,就会导致超参数的值被调整为只拟合测试数据。这样的话,可能就会得到不能拟合其他数据、泛化能力低的模型。
因此,调整超参数时,必须使用超参数专用的确认数据。用于调整超参数的数据,一般称为验证数据(validation data)。
训练数据用于参数(权重和偏置)的学习,验证数据用于超参数的性能评估。为了确认泛化能力,要在最后使用(比较理想的是只用一次)测试数据。
如果是MNIST数据集,获得验证数据的最简单的方法就是从训练数据中事先分割20%作为验证数据,代码如下所示。
这里,分割训练数据前,先打乱了输入数据和教师标签。这是因为数据集的数据可能存在偏向(比如,数据从“0”到“10”按顺序排列等)。这里使用的shuffle_dataset函数利用了np.random.shuffle
接下来,我们使用验证数据观察超参数的最优化方法
6.5.2 超参数的最优化
进行超参数的最优化时,逐渐缩小超参数的“好值”的存在范围非常重要。所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后,多次重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围。
通过重复这一操作,就可以逐渐确定超参数的合适范围。
超参数的范围只要“大致地指定”就可以了。所谓“大致地指定”,是指像0.001(10−3 )到1000(103 )这样,以“10的阶乘”的尺度指定范围
在超参数的最优化中,要注意的是深度学习需要很长时间(比如,几天或几周)。因此,在超参数的搜索中,需要尽早放弃那些不符合逻辑的超参数。于是,在超参数的最优化中,减少学习的epoch,缩短一次评估所需的时间是一个不错的办法。
以上就是超参数的最优化的内容,简单归纳一下,如下所示
反复进行上述操作,不断缩小超参数的范围,在缩小到一定程度时,从该范围中选出一个超参数的值。这就是进行超参数的最优化的一种方法。
6.5.3 超参数最优化的实现
现在,我们使用MNIST数据集进行超参数的最优化。这里我们将学习率和控制权值衰减强度的系数(下文称为“权值衰减系数”)这两个超参数的搜索问题作为对象
如前所述,通过从 0.001(10−3 )到 1000(103 )这样的对数尺度的范围中随机采样进行超参数的验证。这在Python中可以写成10 ** np.random.uniform(-3, 3)。在该实验中,权值衰减系数的初始范围为10−8 到10−4 ,学习率的初始范围为10−6 到10−2 。此时,超参数的随机采样的代码如下所示。
像这样进行随机采样后,再使用那些值进行学习。之后,多次使用各种超参数的值重复进行学习,观察合乎逻辑的超参数在哪里。
下面进行实验,结果如图6-24所示。
图6-24中,按识别精度从高到低的顺序排列了验证数据的学习的变化。
从图中可知,直到“Best-5”左右,学习进行得都很顺利。因此,我们来观察一下“Best-5”之前的超参数的值(学习率和权值衰减系数),结果如下所示。
从这个结果可以看出,学习率在0.001到0.01、权值衰减系数在10−8 到 10−6 之间时,学习可以顺利进行。像这样,观察可以使学习顺利进行的超参数的范围,从而缩小值的范围。然后,在这个缩小的范围中重复相同的操作。这样就能缩小到合适的超参数的存在范围。
6.6 小结
•参 数 的 更 新 方 法,除 了 SGD 之 外,还 有 Momentum、AdaGrad、Adam等方法。
• 权重初始值的赋值方法对进行正确的学习非常重要。
• 作为权重初始值,Xavier初始值、He初始值等比较有效。
• 通过使用Batch Normalization,可以加速学习,并且对初始值变得健壮。
• 抑制过拟合的正则化技术有权值衰减、Dropout等。
• 逐渐缩小“好值”存在的范围是搜索超参数的一个有效方法。