深度学习入门:基于Python的理论与实现——第四章——神经网络的学习

第四章——神经网络的学习

本章的主题是神经网络的学习。这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。本章中,为了使神经网络能进行学习,将导入损失函数这一指标。而学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值,本章我们将介绍利用了函数斜率的梯度法。

  1. 从数据中学习

    神经网络的特征就是可以从数据中学习。所谓“从数据中学习”是指可以由数据自动决定权重参数的值。在实际的神经网络中,参数的数量成千上万,在层数更深的深度学习中,参数的数量甚至可以上亿,想要人工决定这些参数的值是不可能的。本章将介绍神经网络的学习,即利用数据决定参数值的方法,并用Python实现对MNIST手写数字数据集的学习。

    对于线性可分问题,第 2章的感知机是可以利用数据自动学习的。根据“感知机收敛定理”,通过有限次数的学习,线性可分问题是可解的。但是,非线性可分问题则无法通过(自动)学习来解决。

    (1)数据驱动

    数据是机器学习的核心,这种数据驱动的方法,也可以说脱离了过往以人为中心的方法。

    通常要解决某个问题,特别是需要发现某种模式时,人们一般会综合考虑各种因素后再给出回答。“这个问题好像有这样的规律性?”,“不对,可能原因在别的地方。”——类似这样,人们以自己的经验和直觉为线索,通过反复试验推进工作。而机器学习的方法则极力避免人为介入,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则必以往的机器学习方法更能避免人为介入。

    现在我们来思考一个具体的问题,比如如何实现数字“5”的识别。如果让我们自己来设计一个能将5正确分类的程序,就会意外地发现这是一个很难的问题。人可以简单地识别出5,但却很难明确说出是基于何种规律而识别出了5。此外,每个人都有不同的写字习惯,要发现其中的规律是一件非常难的工作。

    因此,与其绞尽脑汁,从零开始想出一个可以识别5的算法,不如考虑通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT(尺度不变特征变换)、SURF(加速稳健特征)和HOG(方向梯度直方图)等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM(支持向量机)、KNN(K最近邻)等分类器进行学习。

    机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始相处算法相比,这种方法可以更高效地解决问题,也能减轻人的负担。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的结果。也就是说,即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。

    到这里,介绍了两种针对机器学习任务的方法:①人想到的算法→答案,②人想到的特征量(SIFT、HOG等)→机器学习(SVM、KNN等)→答案。

    神经网络(深度学习)的方法中不存在人为介入,连图像中包含的重要特征量也都是由机器来学习的。

    深度学习有时也称为端到端机器学习(end-to-end machine learning)。这里所说的“端到端”是指从一端到另一端的意思,也就是从原始数据(输入)中获得目标结果(输出)的意思。

    神经网络的优点是对所有的问题都可以用同样的流程来解决,都说通过不断地学习所提供的数据,尝试发现待求解的问题的模式。也就是说,与待处理的问题无关,神经网络可以将数据直接作为原始数据,进行“端对端”的学习。

    (2)训练数据和测试数据

    本章主要介绍神经网络的学习,在此之前先介绍一下机器学习中有关数据处理的一些注意事项。

    机器学习中,一般将数据分为“训练数据”和“测试数据”两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的“泛化能力”。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为“监督数据”。

    “泛化能力”是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。比如,在识别手写数字的问题中,泛化能力可能会被用在自动读取明信片的邮政编码系统上。此时,手写数字识别就必须具备较高的识别“任意一个人写的任意文字”的能力。如果系统只能正确识别已有的训练数据,那有可能是只学习到了训练数据中的个人的习惯写法。

    因此,仅用一个数据集去学习和评价参数,是无法进行正确评价的。这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。顺便说一下,只对某个数据集过度拟合的状态称为“过拟合”(over fitting)。避免过拟合也是机器学习的一个重要课题。

  2. 损失函数

    神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。神经网络的学习中所用的指标称为“损失函数”(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

    损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。以“性能的恶劣程度”为指标可能会使人感到不太自然,但是如果给损失函数乘上一个负值,就可以解释为“在多大程度上不坏”,即“性能有多好”。并且,“使性能的恶劣程度达到最小”和“使性能的优良程度达到最大”是等价的,不管是用“恶劣程度”还是“优良程度“,做的事情本质上都是一样的。

    (1)均方误差

    可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squared error)。均方误差如下式所示。

    这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。

    用python实现均方误差:

    import numpy as np
    ​
    ​
    def mean_squared_error(y, t):
        return 0.5 * np.sum((y - t) ** 2)
    ​
    ​
    t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
    y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
    print(mean_squared_error(np.array(y), np.array(t)))
    ​
    y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
    print(mean_squared_error(np.array(y), np.array(t)))

    这里的两个例子中,正确解都是“2”,第一个神经网络的输出的最大值是“2”,第二个神经网络的输出的最大值是“7”,第一个例子的损失函数的值更小,和监督数据之间的误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。

    (2)交叉熵误差

    除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示:

    这里,log表示以e为底数的自然对数,yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。因此,该式实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是“2”,预支对应的神经网络的输出是0.6,则交叉熵误差是-log0.6=0.51;若“2”对应的输出是0.1,则交叉熵误差为-log0.1=2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。

    用python代码实现交叉熵误差:

    def cross_entropy_error(y, t):
        delta = 1e-7
        return -np.sum(t * np.log(y + delta))
    ​
    ​
    t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
    y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
    ​
    print(cross_entropy_error(np.array(y), np.array(t)))
    ​
    y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
    print(cross_entropy_error(np.array(y), np.array(t)))

    这里函数内部在计算np.log时,加上了一个微小值delta,这是因为,当出现np.log(0)时,np.log(0)会变为负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。

    (3)mini-batch学习

    机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有100个的话,我们就要把这100个损失函数的总和作为学习的指标。

    前面介绍的损失函数的例子中考虑的都说针对单个数据的损失函数。如果想要求得所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子:

    这里假设数据有N个,tnk表示第n个数据的第k个元素的值(ynk是神经网络的输出,tnk是监督数据)。式子虽然看起来有一些复杂,其实只是把求单个数据的损失函数的式子扩大到了N份数据,不过最后还要除以N进行正规化。通过除以N,可以求单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有一千个或一万个,也可以求得单个数据的平均损失函数。

    另外,MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。

    下面用python编写从训练数据中随机选择指定个数的数据的代码,以进行mini-batch学习。在这之前,先看一下用于读入MNIST数据集的代码:

    import sys, os
    ​
    sys.path.append(os.pardir)
    import numpy as np
    from dataset.mnist import load_mnist
    ​
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    ​
    print(x_train.shape)  # (60000,784)
    print(t_train.shape)  # (60000,10)

    load_mnist函数是用于读入MNIST数据集的函数,这个函数会读入训练数据和测试数据。读入数据时,通过设定参数one_hot_label=True,可以得到one-hot表示(即仅正确解标签为1,其余为0的数据结构)

    读入上面的MNIST数据后,训练数据有60000个,输入数据是784维(28×28)的图像数据,监督数据是10维的数据(“维度”是用来描述每个样本的特征数量或者标签类别数量的)。因此,上面的x_train、t_train的形状分别是(60000,784)和(60000,10)

    要想从这个训练数据中随机抽取10笔数据,我们可以使用Numpy的np.random.choice()写成如下形式:

    # 从这个训练数据中随机抽取10笔数据
    train_size = x_train.shape[0]
    batch_size = 10
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    ​
    print(x_batch.shape)  # (10,784)
    print(t_batch.shape)  # (10,10)

    使用np.random.choice()可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。如下面的实际代码所示,我们可以得到一个包含被选数据的索引的数组。

    >>> np.random.choice(60000, 10)
    array([ 8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260, 21411])

    之后,我们只需指定这些随机选出的索引,取出mini-batch,然后使用这个mini-batch计算损失函数即可。

    mini-batch的损失函数是利用一部分样本数据来近似地计算整体。也就是说,用随机选择的小批量数据(mini-batch)作为全体训练数据的近似值。

    (4)mini-batch版的交叉熵误差的实现

    如何实现对应mini-batch的交叉熵误差呢?只要改良一下之前实现的对应单个数据的交叉熵误差就可以了。这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输入)两种情况的函数

    def cross_entropy_error(y, t):
        if y.ndim == 1:
            t = t.reshape(1, t.size)
            y = y.reshape(1, y.size)
        batch_size = y.shape[0]
        return -np.sum(t * np.log(y + 1e-7)) / batch_size

    这里,y是神经网络的输入,t是监督数据。y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状。并且,当输入为mini-batch时,要用batch的个数进行正规化,计算单个数据的平均交叉熵误差。

    此外,当监督数据是标签形式(非one-hot表示,而是像“2“,”7“这样的标签”时,交叉熵误差可通过如下代码实现:

    # 如果监督数据是标签形式(非one-hot表示(标签表示),比如像“2”,“7”这样的标签)
    def cross_entropy_error_label_encoding(y, t):
        if y.ndim == 1:
            t = t.reshape(1, t.size)
            y = y.reshape(1, y.size)
        batch_size = y.shape[0]
        return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

    实现的要点是,由于one-hot表示中t为0的元素的交叉熵误差也为0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此,t为one-hot表示时通过t*np.log(y)计算的地方,在t为标签形式时,可用np.log(y[np.arange(batch_size),t])实现相同的处理(为了便于观察,这里省略了微小值1e-7)。

    作为参考,简单介绍一下np.log(y[np.arange(batch_size), t])。np.arange(batch_size)会生成一个从0到batch_size-1的数组,比如当batch_size为5时,np.arange(batch_size)会生成一个Numpy数组[0, 1, 2, 3, 4]。因为t中标签是以[2,7,0,9,4]的形式存储的,所以y[np.arange(batch_size), t]能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,y[np.arange(batch_size), t]会生成Numpy数组[y[0, 2], y[1, 7], y[2, 0], y[3, 9], y[4, 4]])。

    (5)为何要设定损失函数

    为什么要导入损失函数呢?以数字识别任务为例,我们想获得的是能提高识别精度的参数,特意再导入一个损失函数不是有些重复劳动吗?也就是说,既然我们的目标是获得使识别度尽可能高的神经网络,那不是应该把识别精度作为指标吗?

    对于这一疑问,可以根据“导数”在神经网络学习中的作用来回答。下一节中会详细说到,在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方,需要计算参数的导数(确切地讲是梯度),然后以这个导数为指引,逐步更新参数的值。

    假设有一个神经网络,现在我们来关注这个神经网络中的某一个权重参数。此时,对该权重参数的损失函数求导,表示的是“如果稍微改变这个权重参数的值,损失函数的值会如何变化”。如果导数的值为负,通过使该权重参数向正方向改变,可以减小损失函数的值;反过来,如果导数的值为正,则通过使该权重参数向负方向改变,可以减小损失函数的值。不过,当导数的值为0时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。

    之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数都会变为0,导致参数无法更新。

    总结一下上面的内容:在进行神经网络的学习时,不能将识别精度作为指标,因为如果以识别精度作为指标,则参数的导数在绝大多数地方都会变为0。

    识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。

    而对于sigmoid函数,不仅函数的输出是连续变化的,曲线的斜率(导数)也是连续变化的。也就是说,sigmoid函数的导数在任何地方都不为0。这对神经网络的学习非常重要。得益于这个斜率不会为0的性质,神经网络的学习得以正确进行。

  3. 数值微分

    梯度法使用梯度的信息决定前进的方向。本节将介绍梯度是什么、有什么性质等内容。在这之前,先介绍一下导数。

    (1)导数

    比如在跑步时,将时间段尽可能缩短,比如计算前1分钟奔跑的距离、前1秒钟奔跑的距离、前0.1秒奔跑的距离……这样就可以获得某个瞬间的变化量(某个瞬时速度)。导数就是表示某个瞬间的变化量。它可以定义成下面的式子:

    接下来参考该式子,用python实现求函数的导数的程序。

    # 不好的实现示例
    def numerical_diff(f, x):
        h = 10e-50
        return (f(x + h) - f(x)) / h

    函数numerical_diff(f,x)的名称来源于数值微分的英文numerical differentiation(所谓数值微分就是用数值方法近似求解函数的导数的过程)。这个函数有两个参数,即“函数f”和“传给函数f的参数x”。乍一看这个实现没有问题,但是实际上这段代码有两处需要改进的地方。

    在上面的实现中,因为想把尽可能小的值赋给h(可以的话,想让h无限接近0),所以h使用了10e-50(有50个连续的0的“0.000……1”)这个微小值。但是,这样反而产生了舍入误差(rounding error)。所谓舍入误差,是指因省略小数的精细部分的数值(比如,小数点后第8位以后的数值)而造成最终的计算结果上的误差。比如,在python中,舍入误差可如下表示:

    >>> np.float32(1e-50) # 0.0

    如上所示,如果用float32类型(32位的浮点数)来表示1e-50,就会变成0.0,无法正确表示出来。也就是说,使用过小的值会造成计算机出现计算上的问题。这就是第一个需要改进的地方,即将微小值h改成10的负四次方,这样就可以得到正确的结果。

    第二个需要改进的地方与函数f的差分有关。虽然上述实现中计算了函数f在x+h和x之间的差分,但是必须注意到,这个计算从一开始就有误差。“真的导数”对应函数在x处的斜率(称为切线),但上述实现中计算的导数对应的是(x+h)和x之间的斜率。因此,真的导数(真的切线)和上述实现中得到的导数的值在严格意义上并不一致。这个差异的出现是因为h不可能无限接近0。

    数值微分含有误差,为了减小这个误差,我们可以计算函数f在(x+h)和(x-h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为中心差分(而(x+h)和x之间的差分称为前向差分)。

    下面基于上述两个要改进的点来实现数值微分(数值梯度)。

    # 基于两个要改进的点来实现数值微分(数值梯度)
    def numerical_diff_new(f, x):
        h = 1e-4  # 0.0001
        return (f(x + h) - f(x - h)) / (2 * h)

    利用微小的差分求导数的过程称为数值微分。而基于数学式的推导求导数的过程,则用“解析性”(analytic)一词,称为“解析性求解”或“解析性求导”。比如,y=x²的导数,可以通过dy/dx=2x解析性地求解出来。解析性求导得到的导数是不含误差的“真的导数”。

    (2)数值微分的例子

    现在试着用上述的数值微分对简单函数进行求导。比如下面的二次函数:

    y=0.01x²+0.1x

    用python实现该函数:

    # 简单的二次函数
    def function_1(x):
        return 0.01 * x ** 2 + 0.1 * x

    接下来绘制这个函数的图像。

    # 绘制函数图像
    import numpy as np
    import matplotlib.pylab as plt
    ​
    x = np.arange(0.0, 20.0, 0.1)  # 以0.1为单位,从0到20的数组x
    y = function_1(x)
    plt.xlabel("x")
    plt.ylabel("f(x)")
    plt.plot(x, y)
    plt.show()

    我们来计算一下这个函数在x=5和x=10处的导数

    # 计算函数在x=5和x=10处的导数
    print(numerical_diff_new(function_1, 5))  # 0.1999999999990898
    print(numerical_diff_new(function_1, 10))  # 0.2999999999986347

    该函数的解析解为df(x)/dx=0.02x+0.1,因此在x=5和x=10处,“真的导数”分别为0.2和0.3。和上面的结果相比,虽然严格意义上并不一致,但误差非常小,误差小到基本上可以认为它们是相等的。

    (3)偏导数

    接下来的函数只是一个计算参数的平方和的简单函数,但它和上例不同的是,这里有两个变量:

    这个式子可以用python来实现:

    # 求参数的平方和的函数
    def function_2(x):
        return x[0] ** 2 + x[1] ** 2  # 或者return np.sum(x**2)

    这里假定向参数输入了一个Numpy数组。函数的内部实现比较简单,先计算Numpy数组中各个元素的平方,再求它们的和。我们来画一下这函数的图像:

    # 生成参数范围
    x = np.linspace(-10, 10, 100)
    y = np.linspace(-10, 10, 100)
    X1, X2 = np.meshgrid(x, y)
    ​
    # 计算函数值
    Y = X1 ** 2 + X2 ** 2
    ​
    # 绘制图像
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.plot_surface(X1, X2, Y, cmap='viridis')
    ​
    # 显示图像
    plt.show()

    np.linspace(-10, 10, 100):这行代码使用Numpy的linspace函数生成了一个包含100个元素的数组,这些元素均匀分布在-10到10之间,用于表示x轴和y轴的取值范围。

    X1, X2 = np.meshgrid(x, y):这行代码使用NumPy的meshgrid函数将x和y轴的取值范围转换成网格矩阵X和Y,用于在二维平面上表示函数的取值范围。

    fig = plt.figure():这行代码创建了一个新的图形窗口。

    ax = fig.add_subplot(111, projection='3d'):这行代码在图形窗口中添加了一个3D子图,用于绘制三维图形。"111"是一个表示子图位置的参数。在这个特定的情况下,"111"表示将图形分割成1行1列,并且当前子图是第一个(也是唯一一个)子图。这种表示方法是Matplotlib中子图位置的一种简写方式。具体来说,"111"中的每一位数字分别代表了行数、列数和子图位置。在这个例子中,"111"表示1行1列的第一个位置,也就是整个图形窗口。这种简写方式在只有一个子图的情况下非常方便,可以让我们直接使用"111"来表示子图的位置,而不必使用更复杂的方式来指定子图的位置。

    ax.plot_surface(X1, X2, Y, cmap='viridis'):这行代码使用plot_surface函数在3D子图中绘制了函数的曲面图,其中X1和X2表示平面上的坐标,Y表示函数的取值,cmap='viridis'表示使用viridis颜色映射。

    现在来求这个函数的导数。这里需要注意的是,该函数中有两个变量,所以有必要区分对哪个变量求导数,即对x0和x1两个变量中的哪一个求导数。另外,我们把这里讨论的有多个变量的函数的导数称为“偏导数”。用数学式表示的话,可以写成∂f/∂x0和∂f/∂x1。

    怎么求偏导数呢?先试着解一下下面两个关于偏导数的问题。

    问题1:求x0=3,x1=4时,关于x0的偏导数∂f/∂x0

    # 求x0=3,x1=4时关于x0的偏导数∂f/∂x0
    def function_temp1(x0):
        return x0 * x0 + 4.0 ** 2.0
    ​
    ​
    print(numerical_diff_new(function_temp1, 3.0))#6.00000000000378

    问题2:求x0=3,x1=4时,关于x1的偏导数∂f/∂x1

    # 求x0=3,x1=4时,关于x1的偏导数∂f/∂x1
    def function_tmp2(x1):
        return 3.0 ** 2.0 + x1 * x1
    ​
    ​
    print(numerical_diff_new(function_tmp2, 4.0))  # 7.999999999999119

    在这些问题中,我们定义了一个只有一个变量的函数,并对这个函数进行了求导。例如,我们定义了一个固定x1=4的新函数,然后对只有变量x0的函数应用了求数值微分的函数。从上面的计算结果可知,问题1的答案是6.00000000000378,问题2的答案是7.999999999999119,和解析解的导数基本一致。

    像这样,偏导数和单变量的导数一样,都是求某个地方的斜率。不过,偏导数需要将多个变量中的某一个变量定位目标变量,并将其他变量固定位某个值。在上例的代码中,为了将目标变量以外的变量固定到某些特定的值上,我们定义了新函数,然后对新定义的函数应用了之前的求数值微分的函数,得到偏导数。

  4. 梯度

    在刚才的例子中,我们按变量分别计算了x0和x1的偏导数。现在,我们希望一起计算x0和x1的偏导数,比如,我们来考虑求x0=3,x1=4时,(x0,x1)的偏导数(∂f/∂x0,∂f/∂x1)。另外,像(∂f/∂x0,∂f/∂x1)这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度可以像下面这样来实现。

    import numpy as np
    ​
    ​
    def numerical_gradient(f, x):
        h = 1e-4  # 0.0001
        grad = np.zeros_like(x)  # 会生成一个形状和x相同、所有元素都为0的数组
    ​
        for idx in range(x.size):
            tmp_val = x[idx]
    ​
            # f(x+h)的计算
            x[idx] = tmp_val + h
            fxh1 = f(x)
    ​
            # f(x-h)的计算
            x[idx] = tmp_val - h
            fxh2 = f(x)
    ​
            grad[idx] = (fxh1 - fxh2) / (2 * h)
            x[idx] = tmp_val  # 还原值
    ​
        return grad

    函数numerical_gradient(f,x)的实现看上去有些复杂,但它执行的处理和求单变量的数值微分基本没有区别。需要补充说明的是,np.zeros_like(x)会生成一个形状和x相同、所有元素都为0的数组。

    tmp_val是一个临时变量,用来存储x数组中当前索引idx对应的元素的值。这是为了在计算f(x+h)和f(x-h)时能够保留原始的x值,以便在计算完梯度后能够将x数组还原到原始状态。

    函数numerical_gradient(f,x)中,参数f为函数,x为Numpy数组,该函数对Numpy数组x的各个元素求数值微分。现在,我们用这个函数实际计算一下梯度。这里我们求点(3,4)、(0,2)、(3,0)处的梯度

    # 求点(3, 4)、(0, 2)、(3, 0)处的梯度
    print(numerical_gradient(function_2, np.array([3.0, 4.0])))  # [6. 8.]
    print(numerical_gradient(function_2, np.array([0.0, 2.0])))  # [0. 4.]
    print(numerical_gradient(function_2, np.array([3.0, 0.0])))  # [6. 0.]
    # 实际上,虽然求到的值是[6.0000000000037801, 7.9999999999991189],但实际输出的是[6., 8.]。这是因为在输出NumPy数组时,数值会被改成“易读”的形式。

    像这样,我们可以计算(x0,x1)在各点处的梯度。这个梯度意味着什么呢?为了更好地理解,我们把f(x0+x1)=x0²+x1²的梯度画在图上。不过,这里我们画的是元素值为负梯度的向量。后面我们将会看到,负梯度方向是梯度法中变量的更新方向。

    # coding: utf-8
    # cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
    import numpy as np
    import matplotlib.pylab as plt
    from mpl_toolkits.mplot3d import Axes3D
    ​
    ​
    def _numerical_gradient_no_batch(f, x):
        h = 1e-4 # 0.0001
        grad = np.zeros_like(x)
        
        for idx in range(x.size):
            tmp_val = x[idx]
            x[idx] = float(tmp_val) + h
            fxh1 = f(x) # f(x+h)
            
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2*h)
            
            x[idx] = tmp_val # 还原值
            
        return grad
    ​
    ​
    def numerical_gradient(f, X):
        if X.ndim == 1:
            return _numerical_gradient_no_batch(f, X)
        else:
            grad = np.zeros_like(X)
            
            for idx, x in enumerate(X):
                grad[idx] = _numerical_gradient_no_batch(f, x)
            
            return grad
    ​
    ​
    def function_2(x):
        if x.ndim == 1:
            return np.sum(x**2)
        else:
            return np.sum(x**2, axis=1)
    ​
    ​
    def tangent_line(f, x):
        d = numerical_gradient(f, x)
        print(d)
        y = f(x) - d*x
        return lambda t: d*t + y
         
    if __name__ == '__main__':
        x0 = np.arange(-2, 2.5, 0.25)
        x1 = np.arange(-2, 2.5, 0.25)
        X, Y = np.meshgrid(x0, x1)
        
        X = X.flatten()
        Y = Y.flatten()
        
        grad = numerical_gradient(function_2, np.array([X, Y]) )
        
        plt.figure()
        plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
        plt.xlim([-2, 2])
        plt.ylim([-2, 2])
        plt.xlabel('x0')
        plt.ylabel('x1')
        plt.grid()
        plt.legend()
        plt.draw()
        plt.show()

    如图所示,f(x0,x1)=x0²+x1²的梯度呈现为有向向量(箭头)。观察该图发现,梯度指向函数f(x0,x1)的“最低处”(最小值),就像指南针一样,所有的箭头都指向同一点。其次,我们发现离“最低处”越远,箭头越大。

    虽然该图中的梯度指向了最低处,但并非任何时候都这样。实际上,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向,这是一个非常重要的性质。高等数学告诉我们,方向导数=cos(θ)×梯度(θ是方向导数的方向与梯度方向的夹角)。因此,所有的下降方向中,梯度方向下降最多。

    (1)梯度法

    机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。

    这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值的最小处。

    函数的极小值、最小值以及被称为鞍点(saddle point)的地方,梯度为0.极小值是局部最小值,也就是限定在某个范围内的最小值。鞍点是从某个方向上看是极大值,从另一个方向上看则是极小值的点。虽然梯度法是要寻找梯度为0的地方,但是那个地方不一定就是最小值(也有可能是极小值或者鞍点)。此外,当函数很复杂且呈扁平状时,学习可能会进入一个(几乎)平坦的地区,陷入被称为“学习高原”的无法前进的停滞期。

    虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的任务中,要以梯度的信息为线索,决定前进的方向。

    此时梯度法就派上用场了。在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后再新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)。梯度法是解决机器学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。

    根据目的是寻找最小值还是最大值,梯度法的叫法有所不同。严格地讲,寻找最小值的梯度法称为梯度下降法(gradient descent method),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但是通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题,因此“下降”还是“上升”的差异本质上并不重要。一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。

    现在尝试用数学式来表示梯度法:

    η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。该式是表示更新一次的式子,这个步骤会反复执行。也就是说,每一步都按该式更新变量的值,通过反复执行此步骤,逐渐减小函数值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过类似的式子(各个变量的偏导数)进行更新。

    学习率需要事先确定为某个值,比如0.01或者0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。

    下面用python来实现梯度下降法:

    # 梯度下降法
    def gradient_descent(f, init_x, lr=0.01, step_num=100):
        x = init_x
        for i in range(step_num):
            grad = numerical_gradient(f, x)
            x -= lr * grad
        return x

    参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。

    使用这个函数可以求函数的极小值,顺利的话,还可以求函数的最小值。

    下面尝试解决一个问题:请用梯度法求f(x0+x1)=x0²+x1²的最小值

    # 用梯度法求f(x0+x1)=x0²+x1²的最小值
    init_x = np.array([-3.0, 4.0])
    print(gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100))  # [-6.11110793e-10  8.14814391e-10]

    这里设置初始值为(-3.0,4.0),开始使用梯度法寻找最小值。最终的结果非常接近(0,0)。实际上,真的最小值就是(0,0),所以说通过梯度法我们基本得到了正确结果。如果用图来表示梯度法的更新过程,则如下图所示,可以发现,原点处是最低的地方,函数的取值一点点在向其靠近(图中虚线是函数的等高线)。

    前面说过,学习率过大或过小都无法得到好的结果,做个实验验证一下:

    # 学习率过大的例子
    init_x = np.array([-3.0, 4.0])
    print(gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100))  # [-2.58983747e+13 -1.29524862e+12]
    ​
    # 学习率过小的例子
    init_x = np.array([-3.0, 4.0])
    print(gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100))  # [-2.99999994  3.99999992]

    实验结果表明,学习率过大的话,会散发成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。

    像学习率这样的参数称为”超参数“。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。

    (2)神经网络的梯度

    神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。比如,有一个只有一个形状为2×3的权重W的神经网络,损失函数用L表示。此时,梯度可以用∂L/∂W表示。用数学式表示的话,如下所示:

    ∂L/∂W的元素由各个元素关于W的偏导数构成。比如,第1行第1列的元素∂L/∂w11表示当w11稍微变化时,损失函数L会发生多大变化。这里的重点是,∂L/∂W的形状和W相同。实际上,该式子中的W和∂L/∂W都是2×3的形状。

    下面以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet的类。

    import sys, os
    ​
    sys.path.append(os.pardir)
    ​
    import numpy as np
    from 交叉熵误差 import cross_entropy_error
    from module3.module3 import softmax
    ​
    ​
    def numerical_gradient(f, x):
        h = 1e-4  # 0.0001
        grad = np.zeros_like(x)
    ​
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            tmp_val = x[idx]
            x[idx] = tmp_val + h
            fxh1 = f(x)  # f(x+h)
    ​
            x[idx] = tmp_val - h
            fxh2 = f(x)  # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2 * h)
    ​
            x[idx] = tmp_val  # 还原值
            it.iternext()
        return grad
    # numerical_gradient(f, x)使用了numpy的nditer对象来遍历输入数组x的所有元素。
    # nditer是numpy中的一个迭代器对象,它可以处理多维数组。
    # 在这段代码中,通过设置flags参数为'multi_index',可以获取当前元素在数组中的多维索引。
    # 这样,这段代码就可以处理多维数组了。
    ​
    class simpleNet:
        def __init__(self):
            self.W = np.random.randn(2, 3)  # 用高斯分布进行初始化
    ​
        def predict(self, x):
            return np.dot(x, self.W)
    ​
        def loss(self, x, t):
            z = self.predict(x)
            y = softmax(z)
            loss = cross_entropy_error(y, t)
    ​
            return loss

    simpleNet类只有一个实例变量,即形状为2×3的权重参数。它有两个方法,一个是用于预测的predict(x)另一个是用于求损失函数值的loss(x,t)。这里参数x接收输入数据,t接收正确解标签。现在我们来试着用一下这个simpleNet:

    net = simpleNet()
    print(net.W)
    x = np.array([0.6, 0.9])
    p = net.predict(x)
    print(p)
    print(np.argmax(p))  # 最大值的索引
    t = np.array([0, 0, 1])  # 正确解标签
    print(net.loss(x, t))

    接下来求梯度。和前面一样,使用numerical_gradient(f,x)求梯度(这里定义的函数f(W)的参数W是一个伪参数。因为numerical_gradient(f,x)会在内部执行f(x),为了与之兼容而定义了f(W))。

    def f(W):
        return net.loss(x, t)
    ​
    ​
    dW = numerical_gradient(f, net.W)
    print(dW)

    numerical_gradient(f,x)的参数f是函数,x是传给函数f的参数。因此,这里参数x取net.W,并定义一个计算损失函数的新函数f,然后把这个新定义的函数传递给numerical_gradient(f,x)。

    numerical_gradient(f,net.W)的结果是dW,一个形状为2×3的二维数组。观察一下dW的内容,例如,会发现∂L/∂W中∂L/∂w11的值大约是0.2,这表示如果将w11增加h,那么损失函数的值会增加0.2h。再比如,∂L/∂w23对应的值大约是-0.5,这表示如果将w23增加h,那么损失函数的值会减小0.5h。因此,从减小损失函数值的观点来看,w23应向正方向更新,w11应向负方向更新。至于更新的程度,w23比w11的贡献要大。

    在python中,lambda表示法是一种用来创建匿名函数的方式。它通常用于需要一个简单函数的地方,而不想正式定义一个函数。lambda函数可以接受任意数量的参数,但只能返回一个表达式的值。

    求出神经网络的梯度后,接下来只需根据梯度算法,更新权重参数即可。

  5. 学习算法的实现

    关于神经网络学习的基础知识,到这里就全部介绍完了。“损失函数”、“mini-batch”、“梯度”、“梯度下降法”等 关键词已经陆续登场,这里我们来确认一下神经网络的学习步骤,顺便复习一下这些内容。神经网络的学习步骤如下所示:

    前提:神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤。

    步骤1:mini-batch

    从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值

    步骤2:计算梯度

    为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。

    步骤3:更新参数

    将权重参数沿梯度方向进行微小更新

    步骤4:重复

    重复步骤1、步骤2、步骤3

    神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini-batch数据,所以又称为随机梯度下降法(stochastic gradient descent)。“随机”指的是“随机选择的”的意思,因此,随机梯度下降法是“对随机选择的数据进行的梯度下降法”。深度学习的很多框架中,随机梯度下降法一般由一个名为SGD的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。

    下面来实现手写数字识别的神经网络。这里以2层神经网络(隐藏层为1层的网络)为对象,使用MNIST数据集进行学习。

    (1)2层神经网络的类

    首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类,实现过程如下所示。

    import sys, os
    
    sys.path.append(os.pardir)
    
    from common.functions import *
    from common.gradient import numerical_gradient
    
    
    class TwoLayerNet:
        # 初始化函数,接收4个参数:
        # input_size输入层的大小(输入层的神经元数),hidden_size隐藏层的大小(隐藏层的神经元数)
        # output_size输出层的大小,weight_init_std权重初始化时的标准差
        def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
            # 初始化权重
            self.params = {}
            # np.random.randn(input_size, hidden_size)生成一个形状为(input_size, hidden_size)的随机矩阵,元素值服从标准正态分布
            # np.zeros(hidden_size)生成一个元素全为0的向量,长度为hidden_size
            self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
            self.params['b1'] = np.zeros(hidden_size)
            self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
            self.params['b2'] = np.zeros(output_size)
    
        # 神经网络的预测函数,接收输入数据x(图像数据),返回神经网络的输出
        def predict(self, x):
            W1, W2 = self.params['W1'], self.params['W2']
            b1, b2 = self.params['b1'], self.params['b2']
    
            # 神经网络的前向传播过程
            a1 = np.dot(x, W1) + b1
            z1 = sigmoid(a1)
            a2 = np.dot(z1, W2) + b2
            y = softmax(a2)
    
            return y
    
        # 计算损失函数的方法,返回值是交叉熵损失
        # 接收输入数据x(图像数据)和监督数据t(正确解标签)
        def loss(self, x, t):
            y = self.predict(x)
            return cross_entropy_error(y, t)
    
        # 计算准确率的方法
        # 接收输入数据x(图像数据)和监督数据t(正确解标签)
        def accuracy(self, x, t):
            y = self.predict(x)
            y = np.argmax(y, axis=1)
            t = np.argmax(t, axis=1)
    
            accuracy = np.sum(y == t) / float(x.shape[0])
            return accuracy
    
        # 计算梯度的方法,基本思想是通过微小的改变输入值,然后观察输出值的变化来估计梯度。
        # 优点:实现简单,理论上可以用于任何函数
        # 缺点:计算量大,效率低(因为需要对每一个输入值都做微小的改变并重新计算输出值)
        # 接收输入数据x和监督数据t,返回权重和偏置的梯度
        def numerical_gradient(self, x, t):
            loss_W = lambda W: self.loss(x, t)
    
            grads = {}
            
            # 注意这里的numerical_gradient,一个是全局函数,一个是这里定义的同名函数,这里并没有构成递归调用和死循环
            grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
            grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
            grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
            grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
    
            return grads
    
        # 另一个计算梯度的方法,使用的是误差反向传播法,比numerical_gradient方法更高效
        # 它只需要对每一层的参数做一次前向和一次反向的计算,就可以得到所有的参数的梯度
        # 接收输入数据x(图像数据)和监督数据t(正确解标签),返回权重和偏置的梯度
        def gradient(self, x, t):
            W1, W2 = self.params['W1'], self.params['W2']
            b1, b2 = self.params['b1'], self.params['b2']
            grads = {}
    
            # 批量数据的数量
            batch_num = x.shape[0]
    
            # forward,前向传播过程,和predict方法中的代码是一样的
            a1 = np.dot(x, W1) + b1
            z1 = sigmoid(a1)
            a2 = np.dot(z1, W2) + b2
            y = softmax(a2)
    
            # backward,反向传播过程,用于计算权重和偏置的梯度
            dy = (y - t) / batch_num  # 计算输出层的误差
            grads['W2'] = np.dot(z1.T, dy)  # 计算W2的梯度
            grads['b2'] = np.sum(dy, axis=0)  # 计算b2的梯度
    
            da1 = np.dot(dy, W2.T)  # 计算隐藏层的误差da1
            dz1 = sigmoid_grad(a1) * da1  # 计算隐藏层的误差dz1
            grads['W1'] = np.dot(x.T, dz1)  # 计算W1的梯度
            grads['b1'] = np.sum(dz1, axis=0)  # 计算b1的梯度
    
            return grads

    params是保存神经网络的参数的字典型变量,params['W1']是第一层的权重,params['b1']是1第一层的偏置。grads是保存梯度的字典型变量(numerical_gradient()方法的返回值),grads['W1']是第一层权重的梯度,grads['b1']是第一层偏置的梯度。

    在初始化函数中,因为进行手写数字识别时,输入图像的大小是784(28×28),输出为10个类别(0到9十个数字),所以指定参数input_size=784,out_put_size=10,将隐藏层的个数hidden_size设置为一个合适的值即可。

    此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。后面我们会详细讨论权重参数的初始化,这里只需要知道,权重使用符合高斯分布的随机数进行初始化,偏置使用0进行初始化。

    numerical_gradient(self,x,t)方法会计算各个参数的梯度,根据数值微分,计算各个参数相对于损失函数的梯度。gradient(self,x,t)是下一章要实现的方法,该方法使用误差反向传播法高效地计算梯度。用误差反向传播法求到的梯度和数值微分的结果基本一致,但可以高效地进行处理。

    (2)mini-batch的实现

    神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。下面,就以TwoLayerNet类为对象,使用MNIST数据集进行学习。

    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    train_loss_list = []
    
    # 超参数
    iters_num = 10000
    train_size = x_train.shape[0]
    batch_size = 100
    learning_rate = 0.1
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    for i in range(iters_num):
        # 获取mini-batch
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
    
        # 计算梯度
        # grad = network.numerical_gradient(x_batch, t_batch)
        grad = network.gradient(x_batch, t_batch) # 高速版
    
        # 更新参数
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
    
        # 记录学习过程
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)

    (3)基于测试数据的评价

    这个损失函数的值,严格的讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。

    神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。

    神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下训练数据和测试数据的识别精度。

    epoch是一个单位,一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000笔训练数据,用大小为100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了。此时,100次就是一个epoch。实际上,一般做法是事先将所有训练数据随机打乱,然后按指定的批次大小,按序生成mini-batch。这样每个mini-batch均有一个索引号,比如此例可以说0,1,2……99,然后用索引号可以遍历所有的mini-batch。遍历一次所有数据,就称为一个epoch。注意,本节中的mini-batch每次都是随机选择的,所以不一定每个数据都会被看到。

    为了正确进行评价,稍稍修改一下前面的代码:

    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    # 超参数
    iters_num = 10000
    train_size = x_train.shape[0]
    batch_size = 100
    learning_rate = 0.1
    
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    # 平均每个epoch的重复次数
    iter_per_epoch = max(train_size / batch_size, 1)
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    for i in range(iters_num):
        # 获取mini-batch
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
    
        # 计算梯度
        # grad = network.numerical_gradient(x_batch, t_batch)
        grad = network.gradient(x_batch, t_batch)  # 高速版
    
        # 更新参数
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
    
        # 记录学习过程
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)
    
        # 计算每个epoch的识别精度
        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            test_acc = network.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print("train acc,test acc | " + str(train_acc) + ", " + str(test_acc))

    在上面的例子中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个epoch的识别精度,是因为如果在for语句的循环中一直计算识别精度,会花费太多时间,并且也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别精度。

    用matplotlib的pyplot进行绘图:

    import matplotlib.pyplot as plt
    ​
    # 绘制图形
    markers = {'train': 'o', 'test': 's'}
    x = np.arange(len(train_acc_list))
    plt.plot(x, train_acc_list, label='train acc')
    plt.plot(x, test_acc_list, label='test acc', linestyle='--')
    plt.xlabel("epochs")
    plt.ylabel("accuracy")
    plt.ylim(0, 1.0)
    plt.legend(loc='lower right')
    plt.show()

    由图可以得到,随着epoch的前进(学习的进行),发现使用训练数据和测试数据评价的识别精度都提高了,并且这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合现象。

  6. 小结

    本章介绍了神经网络的学习。首先,为了能顺利进行神经网络的学习,导入了损失函数这个指标。以这个损失函数为基准,找出使它的值达到最小的权重参数,就是神经网络学习的目标。为了找到尽可能小的损失函数值,介绍了使用函数斜率的梯度法。

    机器学习中使用的数据集分为训练数据和测试数据。

    神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力。

    神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。

    利用某个给定的微小值的差分求导数的过程,称为数值微分。

    利用数值微分,可以计算权重参数的梯度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值