《深度学习图解》正则化和批处理


使用在MNIST上的三层网络

import sys, numpy as np
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data() # 返回一个元组,包含两个元素;第一个元素是训练数据集,第二个元素是测试训练集;每个数据集第一个元素是输入数据,第二个元素是对应的标签数据

'''
x_train[0:1000]:从训练数据集中提取前1000个图像
x_train[0:1000].reshape(1000,28*28):将提取的图像数据进行重塑,将每个图像的形状从28×28转换为1000行(一维数组)
/ 255:将图像数据进行归一化处理,将像素值从0-255的范围缩放到0-1之间
y_train[0:1000]:从训练数据集中提取前1000个图像对应的标签
'''
images, labels = (x_train[0:1000].reshape(1000,28*28) / 255, y_train[0:1000])

one_hot_labels = np.zeros((len(labels),10)) # 创建一个形状为(标签数量, 独热编码长度)的全零矩阵
for i,l in enumerate(labels): # 获取标签列表中的索引i和元素值l
    one_hot_labels[i][l] = 1 # 对应标签的位置设置为1
labels = one_hot_labels # 将转换后的独热编码赋值给标签变量

test_images = x_test.reshape(len(x_test),28*28) / 255 # 重新调整为二维数组,像素值为28×28,归一化处理像素值范围到0-1之间
test_labels = np.zeros((len(y_test),10)) # 这里的10代表10个可能的标签类别
for i,l in enumerate(y_test):
    test_labels[i][l] = 1 # 该图像对应的类别设置为1,表示该图像属于该类别
    
np.random.seed(1) # 设置随机种子为1,这样每次运行程序时生成的随机数序列都是相同的
relu = lambda x:(x>=0) * x # returns x if x > 0, return 0 otherwise
relu2deriv = lambda x: x>=0 # returns 1 for input > 0, return 0 otherwise 其实是relu函数的斜率
alpha, iterations, hidden_size, pixels_per_image, num_labels = (0.005, 350, 40, 784, 10) # 学习率 迭代次数 隐藏层节点个数 每个图像的像素数(表示输入图像的大小) 标签的数量(分类问题的类别数)

weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1 # 随机化0-1层的权重
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1 # 随机化1-2层的权重

for j in range(iterations):
    error, correct_cnt = (0.0, 0) # 初始化误差和正确分类的计数为0
    
    for i in range(len(images)): # 对每个输入图像进行处理
        layer_0 = images[i:i+1] # 将当前图像作为输入传递给第一个隐藏层,注意此种写法返回的是一个二维列表,例如[[7,8,9]]
        layer_1 = relu(np.dot(layer_0,weights_0_1)) # 先计算第一个隐藏层的输出,然后进行relu激活函数筛选节点
        layer_2 = np.dot(layer_1,weights_1_2) # 计算第二个隐藏层的输出,即输出层的预测结果(概率的得分),正向传播完毕

        error += np.sum((labels[i:i+1] - layer_2) ** 2) # 误差累计计算
        correct_cnt += int(np.argmax(layer_2) == \
                                        np.argmax(labels[i:i+1])) # 先使用argmax函数找到输出层最大值的索引以及标签最大值的索引,比较两个索引是否相等,并用int转换为1或者0整数

        layer_2_delta = (labels[i:i+1] - layer_2) # 计算第二层的误差增量
        layer_1_delta = layer_2_delta.dot(weights_1_2.T)\
                                    * relu2deriv(layer_1) # 计算第一层的误差增量
        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta) # 更新权重
        weights_0_1 += alpha * layer_0.T.dot(layer_1_delta) # 更新权重

    sys.stdout.write("\r I:"+str(j)+ \
                     " Train-Err:" + str(error/float(len(images)))[0:5] +\
                     " Train-Acc:" + str(correct_cnt/float(len(images))))

神经网络能够处理包含1000张图像的数据集,并学会将每张输入图像和正确的标签关联起来。

但是它在1000张训练过的图像之外的图像上表现的并不是很好:

if(j % 10 == 0 or j == iterations-1):
    error, correct_cnt = (0.0, 0)

    for i in range(len(test_images)):

        layer_0 = test_images[i:i+1]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        layer_2 = np.dot(layer_1,weights_1_2)

        error += np.sum((test_labels[i:i+1] - layer_2) ** 2)
        correct_cnt += int(np.argmax(layer_2) == \
                                        np.argmax(test_labels[i:i+1]))
    sys.stdout.write(" Test-Err:" + str(error/float(len(test_images)))[0:5] +\
                     " Test-Acc:" + str(correct_cnt/float(len(test_images))) + "\n")
    print()

它的预测准确率只有 70.7%,这被称为测试准确率。这是神经网络在没有训练过的数据集上的准确率,它模拟了神经网络在现实世界表现的性能。

神经网络只学会了在非常特定的输入配置下将输入数据转换为输出数据。如果你给它一些看起来不熟悉的东西,就会给出随机结果。

神经网络中的过拟合

打印一下再训练神经网络每迭代10次时,训练数据集和测试数据集上的准确率:

image-20231201204623117

测试精度在前20次迭代中逐渐提高,但随着训练次数的增加,提高幅度会变慢(不过,训练精度仍在提高)。

这种情况称为过拟合,即神经网络在这种情况下只能识别出它“训练”过的模型的形状:

查看神经网络权重的一种方法是将它看成一个高维的形状。当你进行训练时,这个形状会围绕数据的形状塑造,学习如何区分不同的模式。不幸的是,测试数据集中的图像与训练数据集中的数据中隐含的模式略有不同。这导致了神经网络在很多测试样例上失效。

过拟合如何解决?

如何让神经网络只在信号上进行训练,而忽略噪声?

一种方法是提前停止,事实证明,大量的噪声来自于图像在细粒度上的各种细节,并且对物体而言,大多数信号都是在图像的一般形状(可能还有颜色)中发现的。

即不要让网络训练足够长的时间来学到噪声!

因此提前停止训练是成本最低的正则化形式,正则化是使模型泛化到新的数据点(而不仅是记忆训练数据)的方法的子领域。

正则化是用于在机器学习模型中鼓励泛化的方法的一个子集,通常通过提高模型学习训练数据的细粒度细节的难度来实现。

如何才能知道什么时候应该停止?

唯一能够让我们真正找到合适时间点的方法是对不在训练数据集中的数据运行模型。

某些情况下,如果你使用测试数据来确定何时停止,模型可能会在测试数据集上过拟合。一般来说,我们不会使用测试集来控制训练的过程,而使用验证集

行业标准正则化:dropout

方法:在训练过程中随机关闭神经元(设置为0)

在训练过程中,你将神经网络中的神经元随机设置为0(通常在反向传播中,相同节点上的delta值也应该被设置为0,但从技术角度看,我们并不需要这么做),这使得神经网络只使用整个网络的随机子网络进行训练。

dropout为什么有效?

通过每次随机训练神经网络中的一小部分,dropout能够让一张大网络像小网络一样进行学习——较小的神经网络不会发生过拟合——因为小的神经网络没有太多的表达能力,它们无法抓住那些可能导致过拟合的更细粒度的细节(或噪声),它们只留下了捕捉那些更大、更明显、更高级特性的空间。

当创建一个大型神经网络,但只使用它的一小部分训练的时候会发生什么?它表现得像一个小型神经网络,但是当你随机地对其数以百万计的不同子网络进行训练时,这些子网络叠加起来依旧能够保持整个网络的表现力。

dropout是一种训练一系列网络并对其进行平均的形式

需要记住的是:神经网络总是随机初始化的。为什么这很重要?因为神经网络通过反复试验来学习,这实际上意味着每个神经网络的学习方式都有所不同。虽然学习效果可能没什么区别,但没有两个神经网络是完全相同的(除非它们一开始由于某种随机或有意的原因被设置成完全相同)。

这会带来一个有趣的性质。当过拟合发生时,没有任何两个神经网络会以完全相同的方式过拟合。只有在每张训练图像都会被完美地预测后,过拟合现象才会发生,此时误差为0,神经网络停止学习(即使你不断重复迭代)。但是,因为每
张神经网络都是从随机预测开始,然后逐步调整它的权重来做出更好的预测,所以每张网络不可避免地会犯不同的错误,导致不同的权重增量更新。这最终形成了一个核心概念:

虽然大型非正则化神经网络更可能对噪声过度拟合,但它们不太可能对相同的噪声过拟合。

尽管神经网络是随机生成的,但它们仍然是从学习最大的、最广泛的特征开始的,之后才会捕捉更多关于噪声的信息。

结论是:

如果你训练100个神经网络(所有的初始化都是随机的),它们每个都倾向于捕捉不同的噪声和相似的信号。因此,当这些神经网络出错时,它们犯的错误往往不同。如果你把它们整合在一起,让它们平等地投票,则它们的误差往往相互抵消,最终只展示出它们学到的共同的东西:信号。

dropout的代码

i = 0
layer_0 = images[i:i+1]
dropout_mask = np.random.randint(2,size=layer_1.shape)

layer_1 *= dropout_mask * 2
layer_2 = np.dot(layer_1, weights_1_2)

error += np.sum((labels[i:i+1] - layer_2) ** 2)

correct_cnt += int(np.argmax(layer_2) == np.argmax(labels[i+i+1]))

layer_2_delta = (labels[i:i+1] - layer_2)
layer_1_delta = layer_2_delta.dot(weights_1_2.T) * relu2deriv(layer_1)

layer_1_delta *= dropout_mask

weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)
  • 要在某一层网络上实现dropout(在本例中为layer_1),你需要将layer_1的值乘以由1和0组成的随机矩阵。这样做的效果是通过将layer_1中的某些节点设置为0,可以随机关闭它们。

    注意:dropout_mask使用的是所谓的50%Bernoulli分布,也就是说,dropout_mask中的每个值有50%的机会是1,有另外50%的机会(1-50%=50%)是0。

  • 接下来要做一件看起来有点奇怪的事情,将layer_1乘以2。为什么要这样做?因为layer_2会计算layer_1的加权和,即使这个求和运算会受到权重影响,本质上它仍然是layer_1所有值的和。如果关闭layer_1中一半的节点,那么这个和将减半。因此,layer_2需要增加它对layer_1的敏感度,有点类似音量太低,一个人靠近收音机更近,才能听得更清楚。

    但是在测试时,我们不再使用dropout,音量将恢复正常。这也会让layer_2对layer_1的信号敏感度回到正常。你需要通过将layer_1乘以一个值(即1/开启节点的百分比)来解决这个问题,这个例子中乘数是2。这样,layer_1的信号强度在训练和测试过程中是相同的,不会受到dropout的影响。

  • 同理反向传播时的delta也需要乘以这个随机矩阵。

代码实现:

import numpy, sys
np.random.seed(1)
def relu(x):
    return (x >= 0) * x # returns x if x > 0
                        # returns 0 otherwise

def relu2deriv(output):
    return output >= 0 #returns 1 for input > 0

alpha, iterations, hidden_size = (0.005, 300, 100)
pixels_per_image, num_labels = (784, 10)

weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
    error, correct_cnt = (0.0,0)
    for i in range(len(images)):
        layer_0 = images[i:i+1]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        dropout_mask = np.random.randint(2, size=layer_1.shape)
        layer_1 *= dropout_mask * 2
        layer_2 = np.dot(layer_1,weights_1_2)

        error += np.sum((labels[i:i+1] - layer_2) ** 2)
        correct_cnt += int(np.argmax(layer_2) == np.argmax(labels[i:i+1]))
        layer_2_delta = (labels[i:i+1] - layer_2)
        layer_1_delta = layer_2_delta.dot(weights_1_2.T) * relu2deriv(layer_1)
        layer_1_delta *= dropout_mask

        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
        weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)

    if(j%10 == 0):
        test_error = 0.0
        test_correct_cnt = 0

        for i in range(len(test_images)):
            layer_0 = test_images[i:i+1]
            layer_1 = relu(np.dot(layer_0,weights_0_1))
            layer_2 = np.dot(layer_1, weights_1_2)

            test_error += np.sum((test_labels[i:i+1] - layer_2) ** 2)
            test_correct_cnt += int(np.argmax(layer_2) == np.argmax(test_labels[i:i+1]))

        sys.stdout.write("\n" + \
                         "I:" + str(j) + \
                         " Test-Err:" + str(test_error/ float(len(test_images)))[0:5] +\
                         " Test-Acc:" + str(test_correct_cnt/ float(len(test_images)))+\
                         " Train-Err:" + str(error/ float(len(images)))[0:5] +\
                         " Train-Acc:" + str(correct_cnt/ float(len(images))))

在MNIST数据集上对dropout进行测试

神经网络在没有dropout的时候曾经达到过81.14%的测试正确率,然后逐步下降,在训练结束时测试正确率为70.73%。

添加dropout后,神经网络的行为是这样的:

image-20231201224533210

不仅神经网络的测试准确率达到了82.36%的峰值,而且几乎没有发生像之前那么糟糕的过拟合现象,在训练完成时,测试准确率仍然有81.81%。

注意:dropout还会减慢训练准确率上升的速度,在之前的训练中,训练准确率飞快地上升到100%,并一直保持在这个水平。

这能帮助我们更好地理解dropout的真正含义:它是一种噪声。它使得神经网络在训练数据上的训练过程更加复杂,就像腿上负重跑马拉松一样。训练确实更困难了,但当你在大型比赛中将腿上的负重解开时,你会感觉身轻如燕,跑的更快——因为你训练的过程要困难的多。

批量梯度下降

这是一个提高训练速度和收敛速度的方法。

之前,我们每次用一个训练样例进行训练,并在每次训练之后更新权重。现在,让我们一次用100个训练样例进行训练,在更新权重时使用所有100个样例的权重增量的平均值。

效果:

image-20231201225608427

注意:与之前相比,训练精度的上升趋势更平稳了。这种现象的出现是因为在训练过程中不断地进行平均权重更新。事实证明,对单个样例进行训练,在生成权重更新增量时会有非常大的噪声,因此对这些权重增量更新进行平均可以使学习过程更平滑。

代码示例:

import numpy as np
np.random.seed(1)

def relu(x):
    return (x >= 0) * x # returns x if x > 0

def relu2deriv(output):
    return output >= 0 # returns 1 for input > 0

batch_size = 100
alpha, iterations = (0.001, 300)
pixels_per_image, num_labels, hidden_size = (784, 10, 100)

weights_0_1 = 0.2*np.random.random((pixels_per_image,hidden_size)) - 0.1
weights_1_2 = 0.2*np.random.random((hidden_size,num_labels)) - 0.1

for j in range(iterations):
    error, correct_cnt = (0.0, 0)
    for i in range(int(len(images) / batch_size)):
        batch_start, batch_end = ((i * batch_size),((i+1)*batch_size))

        layer_0 = images[batch_start:batch_end]
        layer_1 = relu(np.dot(layer_0,weights_0_1))
        dropout_mask = np.random.randint(2,size=layer_1.shape)
        layer_1 *= dropout_mask * 2
        layer_2 = np.dot(layer_1,weights_1_2)

        error += np.sum((labels[batch_start:batch_end] - layer_2) ** 2)
        for k in range(batch_size):
            correct_cnt += int(np.argmax(layer_2[k:k+1]) == np.argmax(labels[batch_start+k:batch_start+k+1]))

            layer_2_delta = (labels[batch_start:batch_end]-layer_2)/batch_size
            layer_1_delta = layer_2_delta.dot(weights_1_2.T)* relu2deriv(layer_1)
            layer_1_delta *= dropout_mask

            weights_1_2 += alpha * layer_1.T.dot(layer_2_delta)
            weights_0_1 += alpha * layer_0.T.dot(layer_1_delta)
            
    if(j%10 == 0):
        test_error = 0.0
        test_correct_cnt = 0

        for i in range(len(test_images)):
            layer_0 = test_images[i:i+1]
            layer_1 = relu(np.dot(layer_0,weights_0_1))
            layer_2 = np.dot(layer_1, weights_1_2)

            test_error += np.sum((test_labels[i:i+1] - layer_2) ** 2)
            test_correct_cnt += int(np.argmax(layer_2) == np.argmax(test_labels[i:i+1]))

        sys.stdout.write("\n" + \
                         "I:" + str(j) + \
                         " Test-Err:" + str(test_error/ float(len(test_images)))[0:5] +\
                         " Test-Acc:" + str(test_correct_cnt/ float(len(test_images)))+\
                         " Train-Err:" + str(error/ float(len(images)))[0:5] +\
                         " Train-Acc:" + str(correct_cnt/ float(len(images))))

不同点:

  1. 运行速度更快:因为每个 np.dot 点积函数现在一次可以完成100个向量点积。CPU架构在处理批处理点积方面要快得多。

  2. alpha的值是原来的20倍:因为批量训练可以减少梯度方差

    梯度方差:是指梯度在每次迭代中随机变化的程度。当梯度方差较大时,学习率较小,因为我们不希望梯度在每次迭代中发生太大的变化。当梯度方差较小时,学习率可以更大,因为我们可以更快地更新权重。

    在本例中,我们使用批量大小为100,因此梯度方差较小。因此,我们可以使用学习率为0.001,这比不使用批量训练时使用的学习率(0.0005)大20倍。

    使用较大的学习率可以使神经网络更快地收敛,但也可能导致过拟合。因此,在使用较大的学习率时,我们需要定期进行验证,以确保神经网络没有过拟合。


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录